npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/app-client-search

v1.0.4

Published

Client-side search core interface for molecule.dev — index a list already in the browser, then search it with typo tolerance, field filters (`category:auth`), phrases and exclusions

Downloads

929

Readme

@molecule/app-client-search

Client-side search core interface for molecule.dev.

Index a list the app already holds in memory — a package catalog, a settings page, a table — and search it as the person types: prefix matching, typo tolerance, per-field boosts, and one query grammar every bond understands (category:auth -deprecated "sign in"). Bond a provider (e.g. @molecule/app-client-search-minisearch) at startup, then call {@link createIndex} anywhere.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { createIndex, setProvider } from '@molecule/app-client-search'
import { provider } from '@molecule/app-client-search-minisearch'

setProvider(provider)

const index = createIndex(packages, {
  idField: 'name',
  fields: ['name', 'description', 'exports'],
  filterFields: ['category', 'type', 'stack'],
  boost: { name: 3 },
})

index.search('category:auth oauth') // auth packages mentioning OAuth, best first
index.search('"sign in" -deprecated') // the phrase, minus anything deprecated
index.search({ text: '', filters: [{ field: 'type', values: ['bond'] }] }) // browse mode

Type

core

Installation

npm install @molecule/app-client-search @molecule/app-bond

API

Interfaces

ParseQueryOptions

Options for {@link parseQuery}.

interface ParseQueryOptions {
  /**
   * Fields that `field:value` may target. A `word:thing` whose `word` is not
   * listed is kept as ordinary text (so a search for `re:act` still works).
   * When omitted, every `word:value` is treated as a filter.
   */
  filterFields?: string[]
}

HighlightSegment

A piece of text, marked when it matched a query term.

interface HighlightSegment {
  /** The text of this piece. */
  text: string
  /** Whether it matched. */
  hit: boolean
}

ClientSearchIndexOptions

How an index is built.

interface ClientSearchIndexOptions<T extends ClientSearchDocument = ClientSearchDocument> {
  /** The field holding each document's unique id (stringified). */
  idField: string

  /** The fields full-text search reads. Array values are joined with spaces. */
  fields: string[]

  /**
   * Fields a query may filter on with `field:value` — see {@link parseQuery}.
   * Defaults to `fields`. A filter field need not be a searched field: a
   * `category` used only for filtering is a common case.
   */
  filterFields?: string[]

  /** Per-field weight multipliers, e.g. `{ name: 3, description: 1 }`. */
  boost?: Record<string, number>

  /** Match on term prefixes (`auth` finds `authentication`). Defaults to `true`. */
  prefix?: boolean

  /**
   * Typo tolerance as a fraction of the term length (`0.2` lets a 5-letter
   * term be off by one letter). `false` disables it. Defaults to `0.2`.
   */
  fuzzy?: number | false

  /**
   * Turns a field's value into searchable text. The default joins arrays with
   * spaces and stringifies everything else; `undefined`/`null` become `''`.
   */
  extractField?: (doc: T, field: string) => string
}

ClientSearchFilter

One field:value constraint. Values are OR'd; several filters are AND'd.

interface ClientSearchFilter {
  /** The field the constraint applies to. */
  field: string

  /** Accepted values, compared case-insensitively; a trailing `*` matches a prefix. */
  values: string[]

  /** Documents matching this filter are DROPPED instead of kept (`-field:value`). */
  negate?: boolean
}

ClientSearchQuery

A query in its structured form. {@link parseQuery} produces one from the text a person types; the app may also build one directly.

interface ClientSearchQuery {
  /** The free-text part, with filters, phrases and exclusions already removed. */
  text: string

  /** `"quoted phrases"` that must appear verbatim (case-insensitive) in a searched field. */
  phrases?: string[]

  /** `-terms` that must NOT appear (as a word prefix) in any searched field. */
  exclude?: string[]

  /** `field:value` constraints. */
  filters?: ClientSearchFilter[]

  /** Maximum hits to return. Defaults to the bond's choice (all matches). */
  limit?: number
}

ParsedQuery

What {@link parseQuery} returns: a query plus the text it came from.

interface ParsedQuery extends ClientSearchQuery {
  /** The original text, untouched. */
  raw: string

  phrases: string[]
  exclude: string[]
  filters: ClientSearchFilter[]
}

ClientSearchHit

One matching document.

interface ClientSearchHit<T extends ClientSearchDocument = ClientSearchDocument> {
  /** The document's id (its `idField` value, stringified). */
  id: string

  /** Relevance; higher is better. Browse-mode results (empty text) score `0`. */
  score: number

  /** The document as it was indexed. */
  doc: T

  /** The query terms that matched (after prefix/fuzzy expansion). */
  terms: string[]

  /** The fields the terms matched in. */
  fields: string[]
}

ClientSearchIndex

An index over a set of documents. Built once, searched many times; documents may be added, replaced or removed afterwards.

interface ClientSearchIndex<T extends ClientSearchDocument = ClientSearchDocument> {
  /** The options the index was built with. */
  readonly options: ClientSearchIndexOptions<T>

  /** Number of documents in the index. */
  readonly size: number

  /**
   * Searches the index.
   *
   * A string is parsed with {@link parseQuery} first (so `category:auth oauth`
   * works). A {@link ClientSearchQuery} is used as given. Empty text with
   * filters is browse mode: every document the filters accept, in insertion
   * order, scored `0`. Empty text and no filters returns every document.
   */
  search(query: string | ClientSearchQuery): ClientSearchHit<T>[]

  /** Completions for a partial term, most likely first. */
  suggest(prefix: string, limit?: number): string[]

  /** Adds a document. Adding an id that exists is an error; use `replace`. */
  add(doc: T): void

  /** Replaces the document with the same id, or adds it. */
  replace(doc: T): void

  /** Removes a document by id. Unknown ids are ignored. */
  remove(id: string): void
}

ClientSearchProvider

Client search provider interface. All client-search bonds implement this.

interface ClientSearchProvider {
  /** Provider name identifier (e.g. `'minisearch'`). */
  readonly name: string

  /**
   * Builds an index over `docs`.
   *
   * @param docs - The documents to index.
   * @param options - How to index them.
   * @returns A searchable index.
   */
  createIndex<T extends ClientSearchDocument>(
    docs: T[],
    options: ClientSearchIndexOptions<T>,
  ): ClientSearchIndex<T>
}

Types

ClientSearchDocument

Any record with string keys. Field values may be strings, numbers, booleans or arrays of those.

type ClientSearchDocument = Record<string, unknown>

Functions

setProvider(provider)

Registers a client search provider as the active singleton. Called by bond packages (e.g. @molecule/app-client-search-minisearch) during app startup.

function setProvider(provider: ClientSearchProvider): void
  • provider — The client search provider implementation to bond.

getProvider()

Retrieves the bonded client search provider, throwing if none is configured.

function getProvider(): ClientSearchProvider

Returns: The bonded client search provider.

hasProvider()

Checks whether a client search provider is currently bonded.

function hasProvider(): boolean

Returns: true if a client search provider is bonded.

createIndex(docs, options)

Builds an index using the bonded provider.

function createIndex<T extends ClientSearchDocument>(
  docs: T[],
  options: ClientSearchIndexOptions<T>,
): ClientSearchIndex<T>
  • docs — The documents to index.
  • options — How to index them.

Returns: A searchable index.

parseQuery(raw, options)

Parses search text into terms, phrases, exclusions and field filters.

Grammar (whitespace separates tokens):

  • word — a term; bonds match it by prefix and with typo tolerance.
  • "two words" — a phrase that must appear verbatim.
  • -word / -"two words" — must not appear.
  • field:value — only documents whose field is value (case-insensitive).
  • field:a,b — a OR b. field:"multi word" quotes a value.
  • field:val* — a value prefix.
  • -field:value — everything EXCEPT those documents.
function parseQuery(raw: string, options?: ParseQueryOptions): ParsedQuery
  • raw — The text typed.
  • options — Which fields are filterable.

Returns: The structured query.

tokenize(raw)

Splits text into tokens, keeping quoted runs together (a "b c" d:"e f" → a, "b c", d:"e f").

function tokenize(raw: string): string[]
  • raw — The text.

Returns: Its tokens, quotes still attached.

unquote(s)

Strips one layer of double quotes, tolerating an unclosed quote.

function unquote(s: string): string
  • s — A token or value.

Returns: The text without its quotes.

filterFieldsOf(options)

The fields a query may filter on for an index: filterFields when set, otherwise the searched fields.

function filterFieldsOf<T extends ClientSearchDocument>(
  options: ClientSearchIndexOptions<T>,
): string[]
  • options — The index options.

Returns: The filterable field names.

extractFieldText(doc, field)

The default field extractor: arrays join with spaces, everything else is stringified, nothing becomes ''.

function extractFieldText(doc: ClientSearchDocument, field: string): string
  • doc — The document.
  • field — The field name.

Returns: Searchable text.

matchesFilter(doc, filter)

Whether a document passes a filter. Used by bonds so filter semantics are identical everywhere: a field's string value, or any element of an array value, equals one of the accepted values case-insensitively; a value ending in * matches a prefix instead.

function matchesFilter(doc: ClientSearchDocument, filter: ClientSearchFilter): boolean
  • doc — The document.
  • filter — The constraint.

Returns: true when the document passes.

matchesFilters(doc, filters)

Whether every filter accepts the document.

function matchesFilters(doc: ClientSearchDocument, filters: ClientSearchFilter[]): boolean
  • doc — The document.
  • filters — The constraints (AND'd).

Returns: true when all pass.

matchesText(doc, fields, phrases, exclude, extract)

Whether a document contains every phrase verbatim (case-insensitive) in at least one of the given fields, and none of the excluded terms as a word prefix in any of them.

function matchesText(
  doc: ClientSearchDocument,
  fields: string[],
  phrases: string[],
  exclude: string[],
  extract?: (doc: ClientSearchDocument, field: string) => string,
): boolean
  • doc — The document.
  • fields — The searched fields.
  • phrases — Required phrases.
  • exclude — Forbidden terms or phrases.
  • extract — The field extractor.

Returns: true when the document passes.

formatQuery(parsed)

Puts a parsed query back into text, for editing one part of it (removing a filter chip). Terms and phrases keep their order; exclusions and filters follow.

function formatQuery(parsed: Omit<ParsedQuery, 'raw'>): string
  • parsed — The structured query.

Returns: Text that {@link parseQuery} reads back to the same structure.

highlightMatches(text, terms)

Splits text into plain and matched segments so a UI can mark what matched: each term matches at the start of a word, case-insensitively, longest terms first. No terms yields one plain segment.

function highlightMatches(text: string, terms: string[]): HighlightSegment[]
  • text — The text to mark.
  • terms — The matched terms (a hit's terms).

Returns: The segments, in order.

Available Providers

| Provider | Package | | ------------- | ---------------------------------------- | | Client Search | @molecule/app-client-search-minisearch |

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-bond ^1.0.1

Runtime Dependencies

  • @molecule/app-bond

  • This searches memory, not a server. Hand it the list you already rendered (a few thousand records is fine); for search over a database or millions of rows use @molecule/api-search on the API side.

  • Filter fields are opt-in. field:value only becomes a filter when field is in filterFields (or in fields when filterFields is unset); anything else stays ordinary text, so a query like re:act still searches. List the fields you want filterable, and keep the names short: they are what people type.

  • Filters compare whole values, not substrings. category:auth matches a category of auth or an array containing auth, case-insensitively; use category:auth* for a prefix. Free text is what matches inside descriptions.

  • Empty text is browse mode. With filters it lists every document they accept in insertion order (score 0); with nothing at all it lists every document. Use that for "show the category" instead of a second code path.

  • The index is plain data with methods — build it once per list (useMemo on the docs), not on every keystroke, and replace/remove documents in place when the list changes.

  • This core renders nothing and binds no key. A results list, highlighting and a / shortcut belong to the app (see @molecule/app-client-search-react and @molecule/app-keyboard-shortcuts), styled via getClassMap() and translated with t().

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • [ ] Typing in the search box narrows the list on every keystroke, and the best match is first (a name match beats a description match).
  • [ ] A typo (paymnts) still finds the right records.
  • [ ] field:value narrows to that value, and combining it with text narrows further; an unknown word:thing searches as plain text.
  • [ ] A quoted phrase only matches records containing it verbatim, and a -term removes records containing that term.
  • [ ] Clearing the box restores the full list, with no stale results.