React hooks and components for building search interfaces with Typesense
npm install @jungle-commerce/typesense-reactA powerful, headless React library for building search experiences with Typesense. This package provides comprehensive search state management, advanced filtering, multi-collection search, and automatic schema discovery - all without UI dependencies.
- š React Hooks - Simple hooks like useSearch and useAdvancedFacets for instant search
- š§ TypeScript First - Full type safety and IntelliSense support
- ā” Performance Optimized - Request debouncing, caching, and smart re-rendering
- šØ UI Agnostic - Works with any React UI library (Material-UI, Ant Design, etc.)
- š Advanced Search - Filtering, faceting, sorting, and geo-search out of the box
- š± Multi-Collection - Search across multiple collections simultaneously
- š¤ AI Ready - Claude MCP integration for AI-powered search assistance
- š§ Schema Discovery - Automatically configure search based on your data
š Full Documentation - Comprehensive guides, API reference, and examples
- Getting Started
- Installation and Setup
- Hello World
- Basic Search
- Adding Filters
- Pagination
- API Reference
- Core API Reference
- Hooks API Reference
- Providers Guide
- Context API
- TypeScript Reference
- Migration Guide
- Guides & Tutorials
- Basic Search Tutorial
- Faceted Search Tutorial
- Multi-Collection Search
- Filter Builder Guide
- Sort Builder Guide
- Date Helpers Guide
- Utilities Reference
- Testing Guide
- Integration Patterns
- Troubleshooting
- Examples
- Minimal Search - Simplest possible search implementation
- Product Search - E-commerce product search with filters
- Documentation Search - Search through documentation
- Multi-Collection Search - Search across multiple collections
- Basic Search App - Basic search with TypeScript
- Advanced Filtering - Complex filtering examples
- Multi-Collection Demo - Another multi-collection example
- Testing Documentation
- Testing Guide - Comprehensive testing documentation
- Integration Testing - Integration test setup
- Test Infrastructure - Test infrastructure details
- Code Examples
- Feature Examples - Examples of all features
- Error Handling Examples - Error handling patterns
- Date Filtering Examples - Date filter examples
- Performance Optimization Examples - Performance tips
- API Pattern Documentation
- Core Patterns - Core API patterns
- Hooks Patterns - Hook usage patterns
- Core Examples - Core API examples
- Hooks Examples - Hook examples
- Provider Examples - Provider examples
- Type Examples - TypeScript type examples
``bash`
npm install @jungle-commerce/typesense-react typesense
`tsx
import { TypesenseConfig } from '@jungle-commerce/typesense-react';
const typesenseConfig: TypesenseConfig = {
nodes: [{
host: 'localhost',
port: 8108,
protocol: 'http'
}],
apiKey: 'your-search-api-key',
connectionTimeoutSeconds: 2,
cacheSearchResultsForSeconds: 60 // 1 minute cache
};
`
`tsx
import { TypesenseSearchClient } from '@jungle-commerce/typesense-react';
const client = new TypesenseSearchClient(typesenseConfig);
const schema = await client.retrieveSchema('products');
console.log(schema);
`
`tsx
import React from 'react';
import { SearchProvider, useSearch } from '@jungle-commerce/typesense-react';
function App() {
return (
collection="products"
searchOnMount={true}
>
);
}
function SearchInterface() {
const { state, actions } = useSearch();
return (
Searching...
}{hit.document.description}
$3
`tsx
import { SearchProvider, useSearch, useAdvancedFacets } from '@jungle-commerce/typesense-react';function App() {
const facetConfig = [
{
field: 'category',
label: 'Category',
type: 'checkbox',
disjunctive: true // Allow multiple selections with OR logic
},
{
field: 'price',
label: 'Price Range',
type: 'numeric',
numericDisplay: 'range'
}
];
return (
config={typesenseConfig}
collection="products"
facets={facetConfig}
searchOnMount={true}
>
);
}
function FilteredSearch() {
const { state } = useSearch();
const facets = useAdvancedFacets();
return (
{/ Category Filter /}
Categories
{state.results?.facet_counts?.find(f => f.field_name === 'category')?.counts.map(item => (
))}
{/ Price Range Filter /}
Price Range
type="range"
min="0"
max="1000"
value={facets.numericFilters.price?.min || 0}
onChange={(e) => facets.actions.setNumericFilter('price',
Number(e.target.value),
facets.numericFilters.price?.max || 1000
)}
/>
{/ Results /}
{state.results?.hits.map(hit => (
{hit.document.name}
Category: {hit.document.category}
Price: ${hit.document.price}
))}
);
}
`$3
`tsx
function SortedSearch() {
const { state, actions } = useSearch(); return (
value={state.sortBy}
onChange={(e) => actions.setSortBy(e.target.value)}
>
{/ Results sorted according to selection /}
{state.results?.hits.map(hit => (
{hit.document.name}
))}
);
}
`$3
`tsx
function FacetedSearch() {
const facetConfig = [
{ field: 'brand', label: 'Brand', type: 'checkbox', disjunctive: true },
{ field: 'color', label: 'Color', type: 'select' },
{ field: 'in_stock', label: 'Availability', type: 'checkbox' }
]; return (
config={typesenseConfig}
collection="products"
facets={facetConfig}
searchOnMount={true}
enableDisjunctiveFacetQueries={true} // Enable OR logic for disjunctive facets
>
);
}
function FacetInterface() {
const { state } = useSearch();
const facets = useAdvancedFacets();
return (
{state.facets.map(facetConfig => {
const facetResults = state.results?.facet_counts?.find(
f => f.field_name === facetConfig.field
); if (facetConfig.type === 'select') {
return (
key={facetConfig.field}
value={facets.selectiveFilters[facetConfig.field] || ''}
onChange={(e) => facets.actions.setSelectiveFilter(
facetConfig.field,
e.target.value
)}
>
{facetResults?.counts.map(item => (
))}
);
}
return (
{facetConfig.label}
{facetResults?.counts.map(item => (
))}
);
})}
);
}
`$3
`tsx
function AdvancedSearch() {
const facetConfig = [
{ field: 'category', label: 'Category', type: 'checkbox', disjunctive: true },
{ field: 'price', label: 'Price', type: 'numeric', numericDisplay: 'range' },
{ field: 'rating', label: 'Rating', type: 'numeric', numericDisplay: 'checkbox' },
{ field: 'release_date', label: 'Release Date', type: 'date' },
{ field: 'status', label: 'Status', type: 'select' }
]; return (
config={typesenseConfig}
collection="products"
facets={facetConfig}
searchOnMount={true}
accumulateFacets={true} // Remember facet values across searches
initialState={{
perPage: 24,
sortBy: 'popularity:desc',
additionalFilters: 'in_stock:true', // Native Typesense filter
multiSortBy: [ // Multi-field sorting
{ field: 'popularity', order: 'desc' },
{ field: 'price', order: 'asc' }
]
}}
>
);
}
function FullFeaturedSearch() {
const { state, actions } = useSearch();
const facets = useAdvancedFacets();
// Calculate derived values
const totalResults = state.results?.found || 0;
const totalPages = Math.ceil(totalResults / state.perPage);
const hasNextPage = state.page < totalPages;
const hasPreviousPage = state.page > 1;
return (
{/ Search Input /}
value={state.query}
onChange={(e) => actions.setQuery(e.target.value)}
placeholder="Search..."
/> {/ Additional Filters /}
{/ Multi-field Sort /}
{/ All Facets /}
Filters ({facets.activeFilterCount} active)
{/ Render all configured facets /}
{/ ... facet rendering code ... /}
{/ Results /}
Found {totalResults} results (Page {state.page} of {totalPages})
{state.results?.hits.map(hit => (
{/ Highlighted fields /}
__html: hit.highlight?.name?.snippet || hit.document.name
}} />
))} {/ Pagination /}
onClick={() => actions.setPage(state.page - 1)}
disabled={!hasPreviousPage}
>
Previous
onClick={() => actions.setPage(state.page + 1)}
disabled={!hasNextPage}
>
Next
);
}
`Schema Discovery Examples
$3
`tsx
import { useSchemaDiscovery, SearchProvider } from '@jungle-commerce/typesense-react';function SchemaBasedSearch() {
const { schema, facetConfigs, searchableFields, sortableFields } = useSchemaDiscovery({
// Exclude certain fields from faceting
excludeFields: ['internal_id', 'created_by'],
maxFacets: 10,
includeNumericFacets: true,
includeDateFacets: true,
onSchemaLoad: (schema) => {
console.log('Schema loaded:', schema);
}
});
if (!schema) return
Loading schema...; return (
config={typesenseConfig}
collection="products"
facets={facetConfigs} // Auto-generated facet config
initialSearchParams={{
query_by: searchableFields.join(','), // Auto-detected searchable fields
sort_by: sortableFields[0]?.field // First sortable field
}}
>
);
}
`$3
`tsx
function PatternBasedDiscovery() {
const { schema, facetConfigs } = useSchemaDiscovery({
// Exclude fields by pattern
patterns: {
excludePatterns: [
{ pattern: 'internal', matchType: 'contains' },
{ pattern: '_id', matchType: 'endsWith' }
]
},
// Other configuration
maxFacets: 8,
includeNumericFacets: true,
// Override specific field types
facetOverrides: {
price: { type: 'numeric', numericDisplay: 'range' },
status: { type: 'select' },
created_at: { type: 'date' }
}
}); return (
config={typesenseConfig}
collection="products"
facets={facetConfigs}
>
);
}
`Dynamic Queries with Unknown Fields
$3
`tsx
function DynamicSearch() {
const { schema, facetConfigs, searchableFields } = useSchemaDiscovery(); if (!schema) return
Discovering schema...; return (
config={typesenseConfig}
collection="products"
facets={facetConfigs}
initialSearchParams={{
query_by: searchableFields.join(',') || '*'
}}
searchOnMount={true}
>
);
}
function GenericSearchInterface() {
const { state, actions } = useSearch();
return (
value={state.query}
onChange={(e) => actions.setQuery(e.target.value)}
placeholder={Search in ${state.schema?.fields.length} fields...}
/>
{state.results?.hits.map(hit => (
{/ Dynamically render all fields /}
{Object.entries(hit.document).map(([key, value]) => (
{key}: {JSON.stringify(value)}
))}
))}
);
}
`$3
`tsx
function DynamicFilteredSearch() {
const { schema, facetConfigs } = useSchemaDiscovery({
// Auto-detect facetable fields
maxFacets: 15,
includeNumericFacets: true,
includeDateFacets: true
}); return (
config={typesenseConfig}
collection="products"
facets={facetConfigs}
>
);
}
function DynamicFilters() {
const { state } = useSearch();
const facets = useAdvancedFacets();
return (
{state.facets.map(facetConfig => {
const facetResult = state.results?.facet_counts?.find(
f => f.field_name === facetConfig.field
); // Render appropriate UI based on facet type
switch (facetConfig.type) {
case 'numeric':
return ;
case 'date':
return ;
case 'select':
return ;
default:
return ;
}
})}
);
}
`$3
`tsx
function DynamicSortedSearch() {
const { schema, sortableFields } = useSchemaDiscovery({
collection: 'products',
enabled: true
}); return (
config={typesenseConfig}
collection="products"
initialState={{
sortBy: sortableFields[0]?.field ?
${sortableFields[0].field}:desc : ''
}}
>
);
}function DynamicSortInterface({ sortableFields }) {
const { state, actions } = useSearch();
return (
);
}
`$3
`tsx
function DynamicFacetedSearch() {
const { schema, facetConfigs } = useSchemaDiscovery({
// Maximum facets to generate
maxFacets: 10,
includeNumericFacets: true,
includeDateFacets: true,
// Custom type overrides
facetOverrides: {
status: { type: 'select' },
type: { type: 'select' },
created_at: { type: 'date' },
updated_at: { type: 'date' }
}
}); return (
config={typesenseConfig}
collection="products"
facets={facetConfigs}
accumulateFacets={true}
>
);
}
`$3
`tsx
function FullDynamicSearch() {
const { schema, facetConfigs, searchableFields, sortableFields } = useSchemaDiscovery({
maxFacets: 20,
includeNumericFacets: true,
includeDateFacets: true
}); // Derive field groups from schema
const fieldGroups = React.useMemo(() => {
if (!schema?.fields) return { numericFields: [], booleanFields: [] };
return {
numericFields: schema.fields
.filter(f => ['int32', 'int64', 'float'].includes(f.type))
.map(f => f.name),
booleanFields: schema.fields
.filter(f => f.type === 'bool')
.map(f => f.name)
};
}, [schema]);
return (
config={typesenseConfig}
collection="products"
facets={facetConfigs}
searchOnMount={true}
accumulateFacets={true}
initialSearchParams={{
query_by: searchableFields.join(','),
sort_by: sortableFields[0]?.field
}}
initialState={{
// Dynamic multi-sort based on field types
multiSortBy: [
// First by any boolean "featured" field
...fieldGroups.booleanFields
.filter(f => f.includes('featured'))
.map(f => ({ field: f, order: 'desc' as const })),
// Then by any numeric "score" or "rating" field
...fieldGroups.numericFields
.filter(f => f.includes('score') || f.includes('rating'))
.map(f => ({ field: f, order: 'desc' as const })),
// Finally by name
{ field: 'name', order: 'asc' as const }
]
}}
>
);
}
`Autocomplete Implementation
$3
`tsx
function Autocomplete() {
const [suggestions, setSuggestions] = useState([]);
const [query, setQuery] = useState(''); return (
config={typesenseConfig}
collection="products"
searchOnMount={false}
>
value={query}
onChange={setQuery}
suggestions={suggestions}
onSuggestionsChange={setSuggestions}
/>
);
}
function AutocompleteInput({ value, onChange, suggestions, onSuggestionsChange }) {
const { state, actions } = useSearch();
React.useEffect(() => {
if (state.results && !state.loading) {
// Extract suggestions from results
const newSuggestions = state.results.hits.slice(0, 5).map(hit => ({
id: hit.document.id,
text: hit.document.name,
highlight: hit.highlight?.name?.snippet
}));
onSuggestionsChange(newSuggestions);
}
}, [state.results, state.loading, onSuggestionsChange]);
React.useEffect(() => {
if (value.length >= 2) {
actions.setQuery(value);
} else {
onSuggestionsChange([]);
}
}, [value, actions, onSuggestionsChange]);
return (
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Start typing..."
/>
{suggestions.length > 0 && (
{suggestions.map(suggestion => (
key={suggestion.id}
onClick={() => onChange(suggestion.text)}
dangerouslySetInnerHTML={{
__html: suggestion.highlight || suggestion.text
}}
/>
))}
)}
);
}
`Search-as-you-type Implementation
`tsx
function SearchAsYouType() {
return (
config={typesenseConfig}
collection="products"
searchOnMount={false}
initialSearchParams={{
query_by: 'name,description',
prefix: true, // Enable prefix search
num_typos: 2, // Allow typos
min_len_1typo: 4, // Minimum length for 1 typo
min_len_2typo: 7 // Minimum length for 2 typos
}}
>
);
}function InstantSearch() {
const { state, actions } = useSearch();
return (
value={state.query}
onChange={(e) => actions.setQuery(e.target.value)}
placeholder="Search instantly..."
autoFocus
/>
{state.query && (
{state.loading && Searching...}
{!state.loading && state.results?.found === 0 && (
No results for "{state.query}"
)}
{state.results?.hits.map(hit => (
__html: hit.highlight?.name?.snippet || hit.document.name
}} />
__html: hit.highlight?.description?.snippet || hit.document.description
}} />
))}
)}
);
}
`Multi-Collection Search
$3
`tsx
import { MultiCollectionProvider, useMultiCollectionContext } from '@jungle-commerce/typesense-react';function MultiSearch() {
const collections = [
{
collection: 'products',
queryBy: 'name,description,brand',
weight: 2.0, // Products are twice as important
maxResults: 20
},
{
collection: 'categories',
queryBy: 'name,description',
weight: 1.0,
maxResults: 5
},
{
collection: 'brands',
queryBy: 'name',
weight: 1.0,
maxResults: 5
}
];
return (
config={typesenseConfig}
defaultCollections={collections}
>
);
}
function UnifiedSearch() {
const { state, search, setQuery } = useMultiCollectionContext();
const handleSearch = (query: string) => {
setQuery(query);
search({
query,
mergeStrategy: 'relevance', // Merge by relevance score
normalizeScores: true, // Normalize scores across collections
resultMode: 'interleaved' // Mix results together
});
};
return (
value={state.query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search everything..."
/>
{state.results?.hits.map(hit => (
${hit._collection}-${hit.document.id}}>
{hit._collection}
{hit.document.name || hit.document.title}
Score: {hit._normalizedScore.toFixed(2)}
))}
);
}
`$3
`tsx
function DynamicMultiSearch() {
const [collections, setCollections] = useState([]); // Discover collections dynamically
useEffect(() => {
async function discoverCollections() {
const client = new TypesenseSearchClient(typesenseConfig);
const allCollections = await client.collections().retrieve();
// Configure each collection based on its schema
const configured = await Promise.all(
allCollections.map(async (col) => {
const schema = await client.collections(col.name).retrieve();
const searchableFields = schema.fields
.filter(f => f.type === 'string' || f.type === 'string[]')
.map(f => f.name);
return {
collection: col.name,
queryBy: searchableFields.join(','),
weight: col.name === 'products' ? 2.0 : 1.0,
maxResults: 10
};
})
);
setCollections(configured);
}
discoverCollections();
}, []);
if (collections.length === 0) return
Loading collections...; return (
config={typesenseConfig}
defaultCollections={collections}
>
);
}
`$3
`tsx
function MultiCollectionAutocomplete() {
const collections = [
{
collection: 'products',
queryBy: 'name,sku',
maxResults: 3,
namespace: 'product'
},
{
collection: 'categories',
queryBy: 'name,path',
maxResults: 2,
namespace: 'category'
},
{
collection: 'brands',
queryBy: 'name',
maxResults: 2,
namespace: 'brand'
}
]; return (
config={typesenseConfig}
defaultCollections={collections}
searchOptions={{
debounceMs: 150,
searchOnMount: false
}}
>
);
}
function MultiAutocomplete() {
const { state, search, setQuery } = useMultiCollectionContext();
const [isOpen, setIsOpen] = useState(false);
const handleInputChange = (value: string) => {
setQuery(value);
if (value.length >= 2) {
search({
query: value,
resultMode: 'perCollection', // Group by collection
perCollectionLimit: 5
});
setIsOpen(true);
} else {
setIsOpen(false);
}
};
return (
value={state.query}
onChange={(e) => handleInputChange(e.target.value)}
placeholder="Search products, categories, brands..."
/>
{isOpen && state.results?.hitsByCollection && (
{Object.entries(state.results.hitsByCollection).map(([collection, hits]) => (
{collection}
{hits.map(hit => (
key={hit.document.id}
className="suggestion"
onClick={() => {
// Handle selection based on namespace
console.log(Selected ${hit._namespace}: ${hit.document.name});
setIsOpen(false);
}}
>
{hit.highlight?.name?.snippet || hit.document.name}
))}
))}
)}
$3
`tsx
function MultiCollectionInstantSearch() {
const collections = [
{
collection: 'products',
queryBy: 'name,description',
weight: 2.0,
includeFields: 'id,name,price,image',
maxResults: 10
},
{
collection: 'help_articles',
queryBy: 'title,content',
weight: 1.0,
includeFields: 'id,title,excerpt',
maxResults: 5
}
]; return (
config={typesenseConfig}
defaultCollections={collections}
searchOptions={{
debounceMs: 100,
minQueryLength: 2
}}
>
);
}
function InstantMultiSearch() {
const { state, search, setQuery, clearResults } = useMultiCollectionContext();
useEffect(() => {
if (state.query.length >= 2) {
search({
query: state.query,
enableHighlighting: true,
highlightConfig: {
startTag: '',
endTag: ''
},
// Use round-robin to ensure variety
mergeStrategy: 'roundRobin',
globalMaxResults: 20
});
} else {
clearResults();
}
}, [state.query]);
return (
value={state.query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Start typing to search..."
className="search-input"
/>
{state.loading && Searching...}
{state.results?.hits.map(hit => (
key={${hit._collection}-${hit.document.id}}
className={result-item ${hit._collection}}
>
{hit._collection}
{(hit._normalizedScore * 100).toFixed(0)}% match
{hit._collection === 'products' ? (
) : (
)}
))}
{state.results && state.results.totalFound === 0 && (
No results found for "{state.query}"
)}
function ProductResult({ hit }) {
return (
${hit.document.price}
function ArticleResult({ hit }) {
return (
---
Previous Documentation
The following sections contain additional advanced features and implementation details:
$3
- Advanced Faceting - Multiple filter types with disjunctive support
- UI State Management - Built-in facet UI state handling
- Advanced Filtering - Raw Typesense filter support
- Schema Intelligence - Auto-configuration from schema
- Performance Optimization - Caching and query optimization
- URL State Management - Shareable search URLs
$3
Configure different facet types based on your data:
`tsx
const facetConfig = [
{
field: 'category',
label: 'Category',
type: 'checkbox', // Multi-select checkboxes
disjunctive: true, // OR logic between selections
maxValues: 10, // Maximum values to show
searchable: true, // Enable search within facet
sortBy: 'count' // Sort by count or value
},
{
field: 'price',
label: 'Price',
type: 'numeric', // Numeric range filter
numericDisplay: 'range', // 'checkbox' | 'range' | 'both'
rangeStep: 10 // Step size for range slider
},
{
field: 'created_at',
label: 'Date Added',
type: 'date', // Date range picker
dateFormat: 'YYYY-MM-DD'
},
{
field: 'status',
label: 'Status',
type: 'select' // Single-select dropdown
},
{
field: 'tags',
label: 'Tags',
type: 'custom', // Custom filter type
renderLabel: (value) => value.toUpperCase()
}
];
`$3
The
useFacetState hook manages UI state for facets:`tsx
// Global facet UI state
const facetUI = useFacetState();// Search within facet values
facetUI.setFacetSearch('category', 'elec');
const filteredValues = values.filter(v =>
v.value.toLowerCase().includes(facetUI.getFacetSearch('category'))
);
// Manage facet expansion
facetUI.toggleFacetExpanded('category');
const isExpanded = facetUI.isFacetExpanded('category');
// Track scroll position
facetUI.setFacetScrollTop('category', 100);
const scrollTop = facetUI.getFacetScrollTop('category');
`$3
The package now supports native Typesense filter_by strings and multi-field sorting:
`tsx
// Add raw Typesense filters
actions.setAdditionalFilters('in_stock:true && (category:electronics || category:computers)');// Multi-field sorting
actions.setMultiSortBy([
{ field: 'price', order: 'desc' },
{ field: 'rating', order: 'desc' },
{ field: 'name', order: 'asc' }
]);
// Or use in initial state
initialState={{
additionalFilters: 'featured:true && discount:>0',
multiSortBy: [
{ field: 'popularity', order: 'desc' },
{ field: 'price', order: 'asc' }
]
}}
>
`$3
`tsx
config={config}
collection="products"
performanceMode={true} // Disable expensive features
enableDisjunctiveFacetQueries={false} // Disable parallel facet queries
accumulateFacets={false} // Disable facet accumulation
>
`$3
Build complex filter strings programmatically:
`tsx
import {
buildDisjunctiveFilter,
buildNumericFilter,
buildDateFilter,
combineFilters,
buildMultiSortString,
parseSortString
} from '@jungle-commerce/typesense-react';// Build filters
const categoryFilter = buildDisjunctiveFilter('category', ['Electronics', 'Books']);
const priceFilter = buildNumericFilter('price', 10, 100);
const combined = combineFilters([categoryFilter, priceFilter]);
// Build sort strings
const sortString = buildMultiSortString([
{ field: 'price', order: 'desc' },
{ field: 'name', order: 'asc' }
]); // Returns: "price:desc,name:asc"
`API Reference
For detailed API documentation, see the API Reference section in the original documentation above.
Testing
$3
`bash
Run unit tests
pnpm testRun integration tests (requires Docker)
pnpm test:integrationRun all tests with coverage
pnpm test:all:coverage
`$3
1. Install dependencies:
pnpm install
2. For integration tests: Ensure Docker is installed and running
3. Run tests: Use the commands aboveFor detailed testing documentation, see our Testing Guide.
$3
When using typesense-react in your application:
`tsx
import { SearchProvider } from '@jungle-commerce/typesense-react';
import { render } from '@testing-library/react';const renderWithSearch = (ui: React.ReactElement) => {
return render(
{ui}
);
};
// Test your components
it('should search when typing', async () => {
renderWithSearch( );
// ... test implementation
});
`Claude Integration
This package includes comprehensive Claude MCP (Model Context Protocol) integration documentation:
- Claude Integration Guide
- Claude API Reference
- Claude Examples
Testing
`bash
Run unit tests
pnpm testRun integration tests (requires Docker)
pnpm test:integrationRun all tests with coverage
pnpm test:all:coverage
``For detailed testing documentation, see our Testing Guide.
We welcome contributions! Please see our Contributing Guide.
- GitHub Issues
- NPM Package
- Documentation
MIT Ā© Jungle Commerce