Skip to content

Latest commit

Β 

History

History
440 lines (343 loc) Β· 24.2 KB

File metadata and controls

440 lines (343 loc) Β· 24.2 KB

CLAUDE.md

This file provides guidance to AI coding assistants β€” including Claude Code, GitHub Copilot, and similar tools β€” when working with code in this repository.

Project Overview

Hoist-react is the client-side component of the Hoist web application development toolkit, built by Extremely Heavy Industries (xh.io). It is a library package (not a standalone app) published as @xh/hoist and consumed by Hoist application projects. The server-side counterpart is hoist-core.

  • Language: TypeScript
  • Framework: React with MobX for reactive state management
  • Package manager: pnpm (version pinned via the packageManager field in package.json)

Hoist Developer Tools and Documentation

IMPORTANT: Do not guess at hoist-react APIs, component props, or framework patterns. Hoist-react ships dedicated tools that provide structured access to all framework documentation and TypeScript type information. You MUST use these tools before modifying or extending hoist-react code to understand existing architecture, configuration patterns, and common pitfalls. The package READMEs and concept docs are the authoritative reference for how Hoist works -- skipping them risks producing code that conflicts with established patterns or misses built-in functionality.

Two interfaces are available. Both share the same underlying registries and produce identical output:

MCP Server (hoist-react) -- When working in the hoist-react repository, an MCP server is configured via .mcp.json and is very likely already available. Use the hoist-search-docs, hoist-list-docs, hoist-read-doc, hoist-search-symbols, hoist-get-symbol, and hoist-get-members tools, plus hoist://docs/{id} resources for direct document access.

CLI Tools -- For environments without MCP support, or when you prefer shell commands. These are real bin entries in the hoist-react package.json β€” invoke them exactly as shown with npx:

# Documentation
npx hoist-docs search "grid sorting"         # Search all docs - returns ranked sections
npx hoist-docs read cmp/grid                 # Read a specific doc by ID
npx hoist-docs read cmp/grid --outline       # List a doc's sections with token counts
npx hoist-docs read cmp/grid -s "Sorting"    # Read one section (cheaper than the whole doc)
npx hoist-docs list                          # List all available docs
npx hoist-docs conventions                   # Print coding conventions
npx hoist-docs index                         # Print the documentation catalog

# TypeScript symbols and types
npx hoist-ts search GridModel                # Ranked search: symbols and members, one line each
npx hoist-ts search headerName               # Find which class or config owns a property
npx hoist-ts symbol GridModel                # Import line, signature, docs, member summary
npx hoist-ts members GridModel --filter col  # Members whose name contains "col", with docs

Use search for discovery - one strong keyword works best, ideally an API name (GridModel, persistWith, headerName); camelCase names match their parts. Multi-word queries rank hits by how many terms they match, so extra words narrow rather than exclude ("StoreRecord raw" finds StoreRecord.raw, "panel modal" finds ModalSupportModel). Every hit carries its import path: the package barrel when one exists, otherwise the file. Prefer the barrel path when shown. Members of every exported class and every *Config, *Spec, and *Options interface are searched too, so "groupSortFn" reaches both GridModel and GridConfig; *Props members appear when the query names the component. impl/ and admin/ code is excluded unless you pass --include-internal. Use symbol when you know the exact name - for classes and interfaces it includes a member summary, usually enough to write the code - and members with --filter for member docs. When multiple symbols share a name (e.g. View exists in both cmp/viewmanager and data/cube), pass the file path with --file to disambiguate - the tools will hint when this is needed. Run npx hoist-docs --help and npx hoist-ts --help for full usage.

Recommended workflow: Start with the documentation index (hoist-docs index or hoist://docs/index) to discover available docs. Use the "Quick Reference by Task" table to find the right doc for your goal, then read the relevant README(s). Supplement with TypeScript symbol lookups for precise API details. The docs provide architectural context and common pitfalls; the TypeScript tools provide exact signatures, decorators, and member listings.

GitHub MCP Server (opt-in)

A Docker-based server providing GitHub API tools (issues, PRs, code search, etc.) via the official github-mcp-server image. Configured in .mcp.json but not enabled by default β€” it requires Docker and an authenticated GitHub CLI, which not every developer keeps running.

To enable:

  1. Install and start Docker.
  2. Install the GitHub CLI (brew install gh) and authenticate with gh auth login. The server invokes gh auth token at startup to fetch a token from the macOS Keychain (or gh's credential store on other platforms), so no plaintext token needs to live in your shell environment.
  3. Add "github" to enabledMcpjsonServers in .claude/settings.local.json (local settings merge with the shared settings.json β€” enabling locally does not affect other developers):
    {
      "enabledMcpjsonServers": ["hoist-react", "github"]
    }

If Docker is not running or gh is not authenticated when the server is enabled, Claude Code may show errors on startup β€” remove "github" from your local settings to resolve.

Fallback when not enabled: The gh CLI provides functionally equivalent access to the same operations (gh pr view, gh issue list, gh api, gh pr create, etc.). Prefer gh over crafting raw curl calls to the GitHub API.

JetBrains IntelliJ MCP Server (idea)

IntelliJ registers its own MCP server in .mcp.json under the name idea, providing tools for interacting with the IDE (file navigation, code inspections, refactoring, terminal commands, etc.). It requires a running IDE instance with the MCP server enabled in IntelliJ's settings.

Not enabled by default - the server fails to connect when no IDE is running, which shows as a startup error. Add "idea" to enabledMcpjsonServers in .claude/settings.local.json to enable it for yourself (local settings merge with the shared settings.json):

{
  "enabledMcpjsonServers": ["hoist-react", "idea"]
}

A read-only subset of its tools (search, read, symbol lookup, inspections) is pre-approved in the shared permissions allowlist, so no extra local config is needed once the server is on. Write and execute tools - apply_patch, execute_terminal_command, execute_sql_query, the xdebug_* family - are deliberately left out and still prompt.

IntelliJ rewrites its own entry in .mcp.json on startup. Take its changes rather than reverting them, or it will keep prompting. Note that it hardcodes the default port 64342, so a second IDE instance on another port needs a local override.

hoist-ai plugin (xh@hoist-ai)

The shared .claude/settings.json enables XH's xh@hoist-ai plugin, the same plugin that Hoist applications install. Claude Code offers to install it on your first session here. Three of its skills apply in this repo:

  • xh:clear-writing - house style for CHANGELOG entries, PR descriptions, docs, and comments. Prose written here follows its rules. Lint a draft with its script before you commit it.
  • xh:setup-worktree - provisions a runnable worktree. It knows this repo has no client-app/.
  • xh:using-hoist-react-reference - routes Hoist API questions to the MCP and CLI tools above. It restates what this file already says, so it adds little here. It is the skill that app developers rely on, and this repo is where changes to its tools land first, so keep it enabled.

The other skills are for Hoist applications and do not apply to this library:

  • xh:onboard-app and xh:hoist-upgrade expect an app with a client-app/ folder. To write upgrade notes for a hoist-react release, use this repo's own xh-upgrade-notes skill instead.
  • xh:using-hoist-core-reference covers Grails and Groovy code, which this repo has none of.

The plugin loads from the marketplace copy, not from a sibling ../hoist-ai checkout. To try a local skill change, install the plugin from that path as hoist-ai's own CLAUDE.md describes.

Build Commands

pnpm install                     # Install dependencies
pnpm lint                        # Lint all code (library JS/TS, MCP tools, SCSS)
pnpm lint:code                   # Lint library JavaScript/TypeScript only
pnpm lint:mcp                    # Lint MCP server and CLI tools (mcp/) only
pnpm lint:styles                 # Lint SCSS only
pnpm typecheck                   # Type check library and MCP tools
pnpm test:mcp                    # Run MCP spec scripts, incl. the doc-search golden set

Linting and type-checking are separate concerns, and neither subsumes the other β€” run both. ESLint is configured type-aware, but that only powers its own rules; it never reports TypeScript compiler errors, so a genuine type error passes pnpm lint. CI runs the two as distinct steps.

This is a library β€” it has no dev server or standalone build. To run locally, use a wrapper application project (e.g., Toolbox) that includes @xh/hoist as a dependency.

Architecture

Core Artifacts

The framework is built around three core artifact types:

  1. Models (HoistModel) - State management and business logic classes. Properties are marked with MobX decorators to make them observable. Models form hierarchies reflecting app structure.

  2. Components (hoistCmp) - Functional React components wrapped with Hoist support including MobX reactivity and model lookup. Created via hoistCmp.factory({}).

  3. Services (HoistService) - Singleton classes for data access and app-wide business logic. Installed via XH.installServicesAsync() and accessed as XH.myCustomService.

See /core/README.md for detailed coverage of all three artifact types.

Key Singleton: XH

XH (in core/XH.ts) is the top-level API entry point. It provides:

  • Access to all Hoist services (e.g., XH.configService, XH.fetchService, XH.myCustomService)
  • App metadata (XH.appCode, XH.appVersion)
  • Common operations (XH.toast(), XH.confirm(), XH.handleException())

See /core/README.md for the full XH API and /svc/README.md for built-in service details.

Element Factories vs JSX

Hoist strongly encourages rendering components via element factories (created at component definition time via the hoistCmp.factory util) over JSX. This minimizes XML-style markup and keeps client side codebases anchored in standard TypeScript/JavaScript syntax:

// Element factory style - strongly preferred
panel({
    title: 'Users',
    items: [grid({model: gridModel})],
    bbar: toolbar(button({text: 'Save'}))
})
// JSX also fully supported - but rarely used by XH
< Panel
    title="Users"
    bbar={<Toolbar><Button text={'Save'}/></Toolbar>}
>
    <Grid model={gridModel}/>
</Panel>

Factories can take a config object for props, using the key item/items for children. A shortcut form also exists where factories are passed children directly as arguments, when no other props are required. Factories all support an omit prop for conditional rendering.

Important β€” items in, children out: item/items are Hoist's calling API. Inside a render function, those values arrive as the standard React children prop (because the factory spreads them as rest args to React.createElement). The canonical pattern when authoring a container component is therefore to destructure children from props and pass them downstream as items to an inner factory. See Authoring a Container Component in the core README for the full explanation, examples, and the $item/$items escape hatch for components whose underlying API genuinely has its own items prop.

See /core/README.md for full element factory API including conditional rendering with omit and factory creation.

HoistBase for MobX Integration and Lifecycle

All Hoist artifacts extend HoistBase, which provides:

MobX Integration Conventions

  • addAutorun() / addReaction() - Managed MobX subscriptions (auto-disposed on destroy)
  • TC39 decorators - no makeObservable() call is needed. Declare @observable and @bindable fields with the accessor keyword (e.g. @bindable accessor myProp = null).
  • @observable MobX decorator - Marks properties as observable state. Use the MobX 7 named re-exports from @xh/hoist/mobx for variants - e.g. @observableRef for reference-only.
  • @action MobX decorator - Marks methods that modify observable state
  • @bindable Hoist decorator - Marks properties as observable and generates setter methods automatically marked as @action - e.g., setMyProp(value) for property myProp (Hoist custom decorator). @bindableRef is the reference-only variant. Setter convention: If a class defines an explicit public setFoo() method, call it (it likely has additional logic). Otherwise for auto-generated @bindable setters, prefer direct assignment (model.myProp = value) over calling the generated setter (model.setMyProp(value)).
  • @computed MobX decorator - Marks getter properties as derived/computed state

Memory/lifecycle Management Conventions

  • @managed decorator - marks child objects for automatic cleanup - apply to properties holding HoistBase instances or arrays of such instances.
  • destroy() - Lifecycle cleanup method. HoistBase superclass implementation auto-disposes managed subscriptions and child objects.

See /core/README.md for detailed HoistBase API, persistence support, and common pitfalls.

Promise Conventions

  • Methods returning Promises are suffixed with Async (e.g., loadUsersAsync)
  • Use the Runner chain (this.runner({loadSpec}).linkTo(...).track(...).fetchJson(...)) for masking, activity tracking, and spans - see docs/telemetry.md
  • Promise extensions (catchDefault(), track(), timeout(), linkTo()) are the lower-level API the Runner wraps

Prefer Hoist Input Components Over Raw HTML

Always use Hoist's built-in input components (textInput, numberInput, select, picker, checkbox, switchInput, dateInput, textArea, etc.) rather than raw HTML <input>, <select>, or <textarea> elements. Hoist inputs provide consistent styling, model binding, and proper integration with the framework's theming and layout system. Raw HTML elements require manual wrappers and custom SCSS that duplicate what Hoist already provides.

Platform Support

Components in /desktop/ and /mobile/ are platform-specific. Shared code lives in /cmp/, /core/, /data/, and /svc/.

Code Style

For the full conventions reference β€” import organization, class structure, component patterns, null handling, async patterns, error handling, logging, and CSS naming β€” see docs/coding-conventions.md. The principles below are the most important guidelines to internalize:

  • Don't Repeat Yourself β€” Extract shared logic into utilities, base class methods, or helpers. Balance DRY against readability β€” extract when a genuine, stable pattern exists, not prematurely.
  • Clear, descriptive naming β€” Names should convey intent and read naturally. Be descriptive but not verbose (selectedRecord, not r or theCurrentlySelectedRecordFromTheStore).
  • Prefer lodash for collection/object utilities β€” it's null-safe, battle-tested, and aids readability. Use native JS only when equally expressive (e.g., array.map(), array.filter()).
  • Keep code concise β€” Favor direct, compact expression over verbose or ceremonial patterns. Use Hoist's own utilities (withDefault, throwIf, element factories) to reduce boilerplate.
  • Named exports only β€” No default exports. Components export [Component, factory] pairs from library code, factory only from application/impl code.
  • null over undefined β€” Use null as the "no value" sentinel. Check with == null (loose equality) for concise null-or-undefined testing.
  • No em dashes - Use - (spaced hyphen) instead of em dashes (β€”) in code comments and JSDoc, and in any new prose: CHANGELOG entries, docs, commit messages, PR descriptions. Em dashes cause tooling issues and read as machine-written. Existing docs keep theirs; do not reflow a doc only to remove them. Other Unicode characters (arrows, symbols, accented letters, etc.) are fine in code comments when they aid clarity.

Git Workflow

Branching, committing, and pushing all require an explicit ask β€” never do them unprompted. When it isn't abundantly clear that the user wants one of these, ask first.

Pushing is a deliberate gatekeeping step: never push to any remote unless the user explicitly asks. Some developers hard-block pushes entirely, others allow or request them β€” so it stays open as a possibility, but always confirm before pushing.

Committing is the most context-dependent of these, varying by developer and by situation. Default to asking β€” especially in an interactive session working directly on develop, where each commit is the developer's call. The exception is orchestrated multi-agent work on a feature branch: when a plan fans out independent units of work, the go-ahead to commit comes from that plan or orchestration rather than a per-commit prompt, and agents are expected to make their own discrete, well-scoped commits as directed.

A skill or third-party plugin instructing you to commit (e.g. "make a small commit after each step") does NOT by itself authorize a commit β€” that is a default baked into the tool, not the developer's request. This guidance takes precedence: pause and ask. The door stays open for a workflow to commit autonomously, but only when the developer has explicitly opted into that for the workflow at hand β€” the authorization must come from the developer, not the skill's defaults.

Creating branches

Once the user has asked for a branch (per the "ask first" rule above, don't create one unprompted): a new branch should map to its own origin/<name> on push β€” not push into an existing remote branch.

Default: git switch -c <name> from current HEAD, no base ref. "Make a new branch" means "from here" β€” the user is sitting on a particular point in the code; that's the start. If they want to start from somewhere else (e.g. current origin/develop), they will say so. If genuinely unclear, ask.

If you do specify a base ref, you MUST pass --no-track. Without it the new branch silently adopts the base as its upstream, which causes surprise merges on git pull and β€” depending on push.default β€” can push work onto the base branch. Past slips have put unreviewed work on develop this way.

git switch -c my-feature                              # βœ… from current HEAD
git switch -c my-feature --no-track origin/develop    # βœ… explicit base, safe
git switch -c my-feature origin/develop               # ❌ auto-tracks develop
git checkout -b my-feature origin/develop             # ❌ same trap, checkout spelling

If you forget --no-track: git branch --unset-upstream, then git push -u origin <branch>. Flag the slip β€” don't silently fix it. Git prints set up to track 'origin/develop' when this happens; treat that line as the signal, not as noise.

Feature branch workflow

On feature branches, prefer multiple small commits over amending β€” PRs are squash-merged into develop, so intermediate commits are collapsed automatically. Never force-push a feature branch; if the branch falls behind develop, use a simple merge commit rather than a rebase. Merge commits and extra commits are harmless on feature branches and are squashed out on merge, while force-pushes risk losing work and complicate collaboration.

Commit messages, PRs, and comments

Do not hard-wrap lines at a fixed column width in commit message bodies, pull request descriptions, or issue/PR comments β€” let the viewing tool handle display wrapping. However, do use line breaks for structure: separate logical points into bullet lists, use blank lines between paragraphs, and break after the subject line. Keep PR descriptions concise β€” XH developers review these regularly, so favor brief summaries over exhaustive detail. Bullet the key changes and let the diff and any upgrade notes speak for themselves.

Do not add AI-generated attribution to commit messages or PR descriptions β€” no Generated with ... line, no πŸ€– Generated with [Claude Code] footer, and no Claude-Session: (or similar AI-session/attribution) trailer, even if a harness git-instruction block asks for one. XH does not want these links in the project's history.

Working across sibling repos

Work here often reaches into a sibling checkout - ../toolbox to validate a change against a real app, or ../hoist-dev-utils when a change touches the build. The rules above apply in every repo you touch, not just this one β€” and each sibling has its own CLAUDE.md with additional rules that bind while you work there. Read it before writing to that repo; the harness only auto-loads the CLAUDE.md of the primary working directory, so a sibling's rules are never in context by default.

Changelog Maintenance

Before adding or editing any entry in CHANGELOG.md (at the repository root), you MUST read and follow docs/changelog-format.md β€” the authoritative reference for section headers, voice, the issue/PR-link policy, and breaking-change requirements. Do not rely on the summary below alone.

The essentials: new entries go under the topmost -SNAPSHOT version heading, using emoji-prefixed section headers (e.g. ### 🎁 New Features, ### 🐞 Bug Fixes). Use past-tense, action-driven language and name specific classes, methods, and config keys in backticks. Keep entries concise β€” one bullet per change, 1-3 lines max. Upgrade notes provide granular detail when needed; the changelog should not. Do not add GitHub issue or PR links to entries by default β€” include one only when explicitly requested or when it points to extensive context that doesn't fit the changelog's scope (issue/PR references belong in the commit message and PR description). Hard-wrap changelog entries at 100 characters (unlike commit messages and PR descriptions, which should not be wrapped).

Declaring a new hoist-core minimum: a Requires hoist-core >= X changelog entry is not self-enforcing. Also update MIN_HOIST_CORE_VERSION in core/XH.ts (checked at startup by EnvironmentService) and the matching row in docs/version-compatibility.md. Set the floor only to what the client genuinely cannot run without - features that detect a missing endpoint and degrade belong in that doc's Recommended Core column, not the floor.

Key Dependencies

  • MobX - Reactive state management
  • AG Grid - Data grid (requires separate license for enterprise features)
  • Blueprint - UI component library
  • Router5 - Client-side routing
  • Highcharts - Charting (requires separate license)

Reference Implementation: Toolbox

Toolbox is XH's example application showcasing hoist-react patterns and components. It provides real-world usage examples of models, components, services, and other framework features.

  • GitHub: https://github.com/xh/toolbox
  • Local checkout: ../toolbox (relative to hoist-react root) - likely exists for Hoist library developers only. Note that the client-side code that uses hoist-react is in the ../toolbox/client-app/src directory - focus your attention there.

When working on hoist-react library code or documentation, reference Toolbox for practical examples of how features are used in applications. Note that the local checkout is specific to the Hoist development environment and would not be available to general application developers who have hoist-react as a dependency.