Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .claude/commands/expose-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
description: Thread a new option on an existing connector through all three flavors (JS, React, Vue) + common tests
argument-hint: <connector> <optionName> [short description of what it does]
---

You are exposing a new option through the InstantSearch fan-out. The architecture is **connector (logic) → flavor wrapper → shared UI**, so the option is defined once in the connector and surfaced by each flavor.

**Input:** `$ARGUMENTS`
- First token = the connector (e.g. `connectRefinementList` or just `refinementList`).
- Second token = the new option name (e.g. `clearOnChange`).
- Remaining text = a short description of the behavior, if given.

If the connector or option name is missing or ambiguous, ask before changing files. If the behavior isn't specified, ask what the option should do — do **not** guess at semantics.

This is a checklist for a cross-flavor option rollout. The connector is the single source of truth; React and Vue only *wrap* it, so do the connector first, then the wrappers.

## Steps

1. **Scope the contract first.** Read the connector `packages/instantsearch.js/src/connectors/<name>/connect<Pascal>.ts` and its JS widget `packages/instantsearch.js/src/widgets/<name>/<name>.tsx`. Confirm the option doesn't already exist and settle the exact semantics + the option's type signature. This is the contract every flavor must match.

2. **Connector first** (in `packages/instantsearch.js`):
- Add the option to the connector's widget-params type with a TSDoc comment.
- Implement the behavior in the lifecycle (`init`/`render`/`getWidgetSearchParameters`/etc.).
- Surface it on the **typed render state** if consumers need it (untyped render state is an oxlint error).
- Thread it through the JS widget. Keep it framework-agnostic — no React/Vue/DOM-framework code in the connector.

For a presentational-only variant, **reuse the existing connector** with a distinct `$$widgetType` — never fork connector logic.

3. **If the option changes shared markup/layout** (not just behavior), update the shared `create<Name>Component` in `packages/instantsearch-ui-components` *before* wiring the flavors — its output is the contract they consume.

4. **Thread it through the wrappers** (independent of each other once the connector is settled):
- **React** — the hook `packages/react-instantsearch-core/src/connectors/use<Pascal>.ts` and the component `packages/react-instantsearch/src/widgets/<Pascal>.tsx`, kept in sync and typed.
- **Vue** — `packages/vue-instantsearch/src/components/<Pascal>.{vue,js}` (and `src/mixins/` if wiring lives there), working for **both Vue 2 and Vue 3**.

5. **Tests.** Cross-flavor behavior → add/extend `tests/common/connectors/<name>/` (or `.../widgets/<name>/`) and register it in each flavor's `common-*.test.*` (see `tests/common/README.md`); flavor-specific assertions stay in each package's co-located `__tests__/`. If browser-level behavior is affected, add an e2e spec (see `.claude/rules/e2e.md`).

6. **Verify.** Confirm all three flavors expose the same option with consistent naming/types, then:
```bash
yarn jest packages/instantsearch.js/src/connectors/<name>
yarn jest <the react/vue paths you touched>
yarn type-check
yarn lint:changed
```
(Or run `/preflight`.)

## Notes

- Why this order: the connector is the contract; React and Vue only *wrap* it, so they're independent of each other and safe to do once the connector is settled.
- Don't hand-edit changelogs (Ship.js generates them). Commit message: `feat(<widget>): add <optionName> option`.
30 changes: 30 additions & 0 deletions .claude/commands/preflight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
description: Run the pre-push checklist — lint, types, relevant tests, and a Conventional-Commit sanity check
allowed-tools: Bash(yarn lint:changed), Bash(yarn lint:fix), Bash(yarn type-check), Bash(yarn type-check:*), Bash(yarn jest:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*)
---

Run the InstantSearch pre-push checks against the **current changes** and report a concise pass/fail summary. Don't fix anything silently — surface failures and propose fixes.

## Steps

1. **Scope the change.** `git status` + `git diff --name-only` (and vs. the base branch if on a feature branch) to see which packages/files are touched.

2. **Lint the changed files:** `yarn lint:changed`. If it fails, show the violations; offer `yarn lint:fix` for auto-fixable ones (note that oxlint bans `for-in`/`for-of`/`async` and implicit `any` in library code — those need manual fixes).

3. **Type-check:** `yarn type-check`. If any touched code interacts with the legacy algoliasearch versions, also run `yarn type-check:v3` / `yarn type-check:v4`.

4. **Run the relevant tests** — not the whole suite. Map changed files to their `yarn jest <path>` (e.g. a connector → `yarn jest packages/instantsearch.js/src/connectors/<name>`; a React widget → its `__tests__` path). If a connector's cross-flavor behavior changed, include the matching `tests/common/` path.

5. **Conventional-Commit sanity check.** Look at staged changes / recent commits and confirm the message fits `type(scope): description` (scope = widget/connector or topic like `deps`/`ci`). Flag if it doesn't; suggest a corrected message. Reference issues with `fix #1234` in the body when applicable.

## Output

Report a short checklist:
- ✅/❌ lint:changed
- ✅/❌ type-check (+ v3/v4 if run)
- ✅/❌ tests (list the paths run)
- ✅/❌ commit message

For any ❌, show the failing output and the smallest fix. Don't run the full `yarn test` or `yarn build` unless asked — this is the fast pre-push loop.

Run the checks directly. If a check fails, fix it in the package that owns the failing file before re-running.
1 change: 1 addition & 0 deletions AGENTS.md
80 changes: 80 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# InstantSearch monorepo

Search UI libraries for Algolia across three flavors (vanilla JS, React, Vue), sharing one connector + UI-component core. Yarn 1 workspaces + Lerna (independent versioning).

## Repo map

| Package | What it is |
|---|---|
| `packages/instantsearch.js` | **Core.** Vanilla JS library. **All connectors live here** (`src/connectors/`) and are shared by every flavor. |
| `packages/react-instantsearch-core` | Headless React: hooks (`useSearchBox`, …) that wrap connectors. Framework-agnostic (web/RN). |
| `packages/react-instantsearch` | React DOM widgets = hooks + UI from `instantsearch-ui-components`. |
| `packages/vue-instantsearch` | Vue 2/3 components wrapping connectors. |
| `packages/instantsearch-ui-components` | **Shared UI layer.** Framework-agnostic widget markup + `ais-*` classes via factory components (`create<Name>Component({ createElement, Fragment })`); reused by React directly, Vue via `renderCompat`, JS via the Preact renderer. Newer widgets get their layout here; older ones still define it in `instantsearch.js/src/components/`. |
| `packages/algoliasearch-helper` | Low-level search-parameter/state manager underneath connectors. Mature, separately-versioned; **rarely the place for a change** — fix the connector instead. See its `CLAUDE.md`. |
| `packages/instantsearch.css` | Default themes. |
| `packages/react-instantsearch-nextjs` / `-router-nextjs` | Next.js App Router / Pages Router integrations. |
| `packages/create-instantsearch-app`, `instantsearch-cli`, `instantsearch-codemods` | Scaffolding / CLI / migration tooling. |

Architecture in one line: **connector (logic, in instantsearch.js) → flavor wrapper (JS widget / React hook+component / Vue component) → shared UI components**. Change behavior in the connector; change markup in the UI layer.

## Where work goes (per layer)

Because behavior lives in the connector and the flavors only wrap it, most changes have a natural home — and the per-package `CLAUDE.md` files carry each layer's conventions:

| Work in… | Package |
|---|---|
| Connectors, JS widgets, runtime (`src/lib`), legacy Preact components (`src/components`), types | `packages/instantsearch.js` |
| Shared framework-agnostic markup / `ais-*` classes | `packages/instantsearch-ui-components` |
| React widgets/hooks + Next.js integrations | `react-instantsearch` / `react-instantsearch-core` / `*-nextjs` |
| Vue components (Vue 2 **and** 3) | `vue-instantsearch` |

For a **cross-flavor change**, settle the contract in the connector first, then update the React and Vue wrappers, then the common tests. If the change is to **shared markup/layout** (not behavior), it lives in `instantsearch-ui-components` and the flavors consume the result — a class/structure change there ripples to all flavors and `instantsearch.css` at once.

**Adding or surfacing a connector option is always cross-flavor work** — settle the contract in the connector first, then thread it through React **and** Vue, then add/extend common tests. Don't wire one flavor and stop. The **`/expose-option <connector> <option>`** command walks this recipe; **`/preflight`** runs the pre-push lint/types/tests/commit check; the `/port-widget` skill adds a brand-new widget across flavors.

## Commands

Node `20.19.0` (`.nvmrc`). Use Yarn (v1).

```bash
# Build
yarn build # all packages (lerna)
yarn workspace <pkg> build # single package, e.g. yarn workspace instantsearch.js build

# Unit tests (Jest, jsdom)
yarn jest <path-or-pattern> # fastest loop: run one file/pattern at root
yarn jest packages/<pkg> # one package's suite (flavor pkgs have no `test` script — scope jest by path)
yarn jest common-widgets # shared cross-flavor widget suites (JS+React+Vue); -connectors for connectors
yarn jest common-widgets -t "Chat widget common tests" # scope to one widget (label = "<Widget> widget common tests")
yarn test # everything (slow)

# Lint (oxlint) + format (prettier) + types
yarn lint:changed # lint only changed files — use this while iterating
yarn lint:fix # auto-fix
yarn type-check # tsc + per-package (also :v3 / :v4 for legacy algoliasearch)

# E2E — see .claude/rules/e2e.md (Playwright). Examples must be built first:
yarn website:examples && E2E_FLAVOR=react E2E_BROWSER=chromium yarn test:e2e
```

## Conventions

- **TypeScript strict.** No implicit any; unused vars/params error. Avoid `for-in`/`for-of` and `async` in library code (oxlint enforces).
- **Tests** co-locate in `__tests__/` next to source — Jest picks up *any* file under `__tests__/` (the default `testMatch`), so some legacy tests are plain `*.js`; name new ones `*.test.ts(x)`. Prefer focused assertions; use inline snapshots sparingly (initial render only). Mocks/helpers come from `@instantsearch/mocks` and `@instantsearch/testutils`.
- **Cross-flavor tests** live in `tests/common/{widgets,connectors}/<name>/` and each flavor registers them in its `common-widgets.test.*` / `common-connectors.test.*`. Run with `yarn jest common-widgets` (all three flavors) and scope to one widget via `-t "<Widget> widget common tests"`. This is the primary test surface for newer widgets like Autocomplete and Chat. See `tests/common/README.md`.
- **Commits: Conventional Commits** — `type(scope): description`, scope = widget/connector or topic (`deps`, `ci`). e.g. `fix(searchbox): increase magnifying glass size`, `feat(hits): add custom rendering`. Reference issues with `fix #1234` in the body.
- **Branches:** target `master`; `vX` branches are critical-fix-only. Branch names: `fix/<issue>`, `feat/<name>`.
- **Releases** are automated via Ship.js (`yarn release`); changelogs are generated from commits — don't hand-edit them.

## Keep these docs alive

If you discover something durable and non-obvious while working — a gotcha that cost you a debugging detour, a convention, a non-obvious file location, a cross-flavor constraint — **propose** adding it to the right doc (package `CLAUDE.md` if package-specific, this file if repo-wide, `.claude/rules/e2e.md` for e2e). Surface the proposed edit for the user to approve rather than editing silently. Bar: it must generalize beyond the current task (same bar as a good code comment) and not already be documented.

## Reference docs (read before relevant work — don't duplicate here)

- `CONTRIBUTING.md` — full process, commit/branch rules, package-import flow.
- `.claude/rules/e2e.md` — E2E testing (Playwright helpers, flavors, debugging).
- `.claude/skills/port-widget/` — adding a widget across all three flavors (the `/port-widget` skill).
- `tests/common/README.md` — shared cross-flavor test architecture.
- Per-package `CLAUDE.md` in `instantsearch.js`, `instantsearch-ui-components`, `react-instantsearch`, `vue-instantsearch` for package-specific layout.
23 changes: 23 additions & 0 deletions packages/algoliasearch-helper/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# algoliasearch-helper

The **low-level search-state manager** that sits *underneath* InstantSearch connectors. It's a separately-published, long-stable npm package (`algoliasearch-helper`, own semver — currently 3.x) that wraps the `algoliasearch` JS client with a higher-level, stateful API: it tracks search parameters, builds the actual Algolia queries, parses responses, and emits events. Every connector in `instantsearch.js` reads and writes this helper's state — but the connector is the layer you change, not this one.

> **⚠️ Rarely the right place for a change/fix/update.** This package is mature and broadly depended on — InstantSearch (all flavors) plus other Algolia front-end libraries build on it. Almost all widget/connector behavior is implemented *on top of* the helper, not in it. Touch it **only** for a genuine state-manager or response-parsing bug, or to add a real new search-parameter primitive. Any change here is a **public-API, independently-versioned** change with a blast radius far beyond this repo — flag it explicitly and prefer fixing the connector instead.

## What's in it

Entry: `algoliasearchHelper(client, index, opts)` → an `AlgoliaSearchHelper` (an `EventEmitter`). Core pieces under `src/`:

- **`SearchParameters/`** — the **immutable** query state (query, `facets`/`disjunctiveFacets`/`hierarchicalFacets`, numeric & tag refinements, `page`, etc.). Setters return a *new* instance; never mutate in place. This is the object connectors mutate via `getWidgetSearchParameters`.
- **`SearchResults/`** — parses the raw Algolia response into a richer structure: facet values, `facets_stats`, and hierarchical facet trees (`generate-hierarchical-tree.js`).
- **`requestBuilder.js`** — turns `SearchParameters` into the actual query payload(s); notably **splits disjunctive faceting into multiple queries**.
- **`DerivedHelper/`** — sub-requests derived from a main helper (federated / multi-index search); this is what powers the `index` widget.
- **`RecommendParameters/` + `RecommendResults/`** — the equivalent state/results pair for the Recommend API.
- **`functions/`** — small internal lodash-style utilities (`merge`, `omit`, `orderBy`, …).

Events emitted by the helper (what InstantSearch's lifecycle listens to): `change`, `search`, `result`, `error`, `fetch`, `searchOnce`, `searchForFacetValues`, `searchQueueEmpty`, `recommendQueueEmpty`.

## Conventions that differ from the rest of the monorepo

- **Plain CommonJS JavaScript**, not the TS-strict source the other packages use. Public types are **hand-written** in `index.d.ts` — update them by hand if you change the surface.
- Has its own **`README.md` + `CHANGELOG.md`** (don't fold into the monorepo's). Build is Rollup (`yarn workspace algoliasearch-helper build`); tests run via its own `scripts/test.sh` (`yarn workspace algoliasearch-helper test`).
46 changes: 46 additions & 0 deletions packages/instantsearch-ui-components/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# instantsearch-ui-components

The **framework-agnostic UI layer** — the shared markup/layout for widgets, reused by every flavor. This is where a widget's DOM structure and `ais-*` CSS classes live for **newer** widgets. (Older widgets still define their layout per-flavor; see "Old vs new" below.)

## The factory + renderer pattern

Components are **not** React/Preact/Vue components directly. Each is a factory that takes a renderer and returns a component:

```tsx
/** @jsx createElement */
import { cx } from '../lib';
import type { Renderer, ComponentProps } from '../types';

export function createHitsComponent({ createElement, Fragment }: Renderer) {
return function Hits(props) {
return <div className={cx('ais-Hits', props.classNames.root)}>…</div>;
};
}
```

- **`Renderer` = `{ createElement, Fragment }`** is *injected* by the consumer (`src/types/Renderer.ts`). Defaults are Preact's, but React passes `react`'s `createElement` and Vue passes its own `h` (wired up via the `renderCompat` helper in `vue-instantsearch`). **Never import `preact`/`react`/`vue` directly** — go through the injected renderer. The `/** @jsx createElement */` pragma at the top of each file is what wires JSX to the injected function.
- Class names go through **`cx`** (`src/lib/cx.ts`) and follow the `ais-<Widget>-<element>` contract that `instantsearch.css` themes — treat those class names as a public API.
- Props are typed with `ComponentProps<'div'>`-style helpers from `src/types`; expose a `classNames` partial so consumers can extend styling.

## Layout

- `src/components/<Name>.tsx` — a `create<Name>Component` factory. Exported from `src/components/index.ts` → `src/index.ts`.
- Grouped families: `src/components/autocomplete/`, `src/components/chat/`, `src/components/recommend-shared/` (+ the recommend widgets `RelatedProducts`, `FrequentlyBoughtTogether`, `LookingSimilar`, `TrendingItems`, `TrendingFacets`).
- `src/lib/` — `cx`, `stickToBottom`, shared `utils`. `src/types/` — `Renderer`, `ComponentProps`, `Hooks`, `Recommend`, `shared`.
- Runtime deps are minimal on purpose (`markdown-to-jsx`, `@swc/helpers`); **no framework as a dependency**.

## Old vs new (important)

- **New widgets:** layout lives here and is consumed by all flavors — React via `import { create<Name>Component } from 'instantsearch-ui-components'`, Vue via the `renderCompat` helper, JS via the Preact renderer.
- **Older widgets:** layout was defined per-flavor — JS in `packages/instantsearch.js/src/components/` (Preact), React in `packages/react-instantsearch/src/ui/`. The migration direction is to consolidate that markup here; some JS-package components are now thin wrappers that import from this package.
- When adding/changing **shared** layout, do it here. When touching a widget that still defines its own markup, check whether it should be migrated rather than forked.

## Working here

```bash
yarn workspace instantsearch-ui-components build # tsc/rollup + build:types
yarn jest packages/instantsearch-ui-components/src/components/__tests__/<Name> # from repo root
```

- Tests are co-located in `src/components/__tests__/`. Test the component by passing a concrete renderer (e.g. Preact's) — assert rendered structure and class names, not implementation.
- A markup or class-name change here **ripples to React, Vue, and JS at once** — verify consumers and keep the `ais-*` contract stable (a class rename is a breaking change that also touches `instantsearch.css`).
Loading
Loading