diff --git a/jest.config.js b/jest.config.js index 63deae4af20..07d8a866890 100644 --- a/jest.config.js +++ b/jest.config.js @@ -54,6 +54,9 @@ const config = { '^instantsearch.js$': '/packages/instantsearch.js/src/', '^instantsearch.js/es(.*)$': '/packages/instantsearch.js/src$1', '^instantsearch.js/(.*)$': '/packages/instantsearch.js/$1', + '^instantsearch-core$': '/packages/instantsearch-core/src/', + '^instantsearch-core/dist/es(.*)$': + '/packages/instantsearch-core/src$1', '^instantsearch-ui-components$': '/packages/instantsearch-ui-components/src/', '^instantsearch-ui-components/(.*)$': diff --git a/packages/algolia-experiences/package.json b/packages/algolia-experiences/package.json index 6b59e6b1edd..04b94443a64 100644 --- a/packages/algolia-experiences/package.json +++ b/packages/algolia-experiences/package.json @@ -10,7 +10,8 @@ ], "dependencies": { "algoliasearch": "5.1.1", - "instantsearch.js": "4.103.0" + "instantsearch.js": "4.103.0", + "instantsearch-core": "0.1.0" }, "devDependencies": { "@instantsearch/testutils": "1.98.0" diff --git a/packages/algolia-experiences/src/banner.tsx b/packages/algolia-experiences/src/banner.tsx index e38ec6904e0..dfac3f107b1 100644 --- a/packages/algolia-experiences/src/banner.tsx +++ b/packages/algolia-experiences/src/banner.tsx @@ -4,11 +4,11 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import type { Banner } from 'algoliasearch-helper'; +import type { HitsConnectorParams } from 'instantsearch-core'; import type { ComponentProps, HitsClassNames, } from 'instantsearch-ui-components'; -import type { HitsConnectorParams } from 'instantsearch.js/es/connectors/hits/connectHits'; export type BannerWidgetParams = { container: HTMLElement; diff --git a/packages/algolia-experiences/src/render.tsx b/packages/algolia-experiences/src/render.tsx index 166f10e5054..df28ea20701 100644 --- a/packages/algolia-experiences/src/render.tsx +++ b/packages/algolia-experiences/src/render.tsx @@ -1,5 +1,5 @@ /** @jsx h */ -import { getPropertyByPath } from 'instantsearch.js/es/lib/utils'; +import { getPropertyByPath } from 'instantsearch-core'; import { carousel } from 'instantsearch.js/es/templates'; import { index, panel } from 'instantsearch.js/es/widgets'; import { h, Fragment } from 'preact'; @@ -17,7 +17,7 @@ import type { TemplateText, TemplateWidgetTypes, } from './types'; -import type { Widget } from 'instantsearch.js'; +import type { Widget } from 'instantsearch-core'; import type { ComponentChildren, JSX } from 'preact'; export function injectStyles() { diff --git a/packages/algolia-experiences/src/setup-instantsearch.ts b/packages/algolia-experiences/src/setup-instantsearch.ts index 2798ba98c56..ea08f2bb092 100644 --- a/packages/algolia-experiences/src/setup-instantsearch.ts +++ b/packages/algolia-experiences/src/setup-instantsearch.ts @@ -9,7 +9,7 @@ import { configToIndex, injectStyles } from './render'; import { error } from './util'; import type { Settings } from './get-information'; -import type { IndexWidget } from 'instantsearch.js'; +import type { IndexWidget } from 'instantsearch-core'; declare global { interface Window { diff --git a/packages/instantsearch-core/README.md b/packages/instantsearch-core/README.md new file mode 100644 index 00000000000..e19a0d78a5d --- /dev/null +++ b/packages/instantsearch-core/README.md @@ -0,0 +1,3 @@ +# instantsearch-core + +Internal shared package for InstantSearch flavors. This package is published for dependency resolution and type resolution, but it is not intended for direct end-user use. diff --git a/packages/instantsearch-core/package.json b/packages/instantsearch-core/package.json new file mode 100644 index 00000000000..e38ff5cb7c2 --- /dev/null +++ b/packages/instantsearch-core/package.json @@ -0,0 +1,52 @@ +{ + "name": "instantsearch-core", + "version": "0.1.0", + "description": "Internal shared package for InstantSearch flavors. Not intended for direct use.", + "types": "dist/es/index.d.ts", + "main": "dist/cjs/index.js", + "module": "dist/es/index.js", + "type": "module", + "exports": { + ".": { + "types": "./dist/es/index.d.ts", + "import": "./dist/es/index.js", + "require": "./dist/cjs/index.js" + } + }, + "sideEffects": false, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/algolia/instantsearch" + }, + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, + "keywords": [ + "algolia", + "instantsearch", + "search" + ], + "files": [ + "README.md", + "dist" + ], + "scripts": { + "clean": "rm -rf dist", + "build": "BABEL_ENV=es,rollup BUILD_FORMAT=esm rollup -c rollup.config.mjs && BABEL_ENV=rollup BUILD_FORMAT=cjs rollup -c rollup.config.mjs && yarn build:types", + "build:types": "tsc -p ./tsconfig.declaration.json --outDir ./dist/es", + "version": "./scripts/version.cjs", + "prepare": "yarn build", + "watch:es": "BABEL_ENV=es,rollup BUILD_FORMAT=esm rollup -c rollup.config.mjs --watch" + }, + "dependencies": { + "@algolia/events": "^4.0.1", + "@swc/helpers": "0.5.18", + "@types/dom-speech-recognition": "^0.0.1", + "@types/qs": "^6.5.3", + "algoliasearch-helper": "3.29.1", + "qs": "^6.5.1", + "search-insights": "^2.17.2" + } +} diff --git a/packages/instantsearch-core/rollup.config.mjs b/packages/instantsearch-core/rollup.config.mjs new file mode 100644 index 00000000000..93fd8068f9b --- /dev/null +++ b/packages/instantsearch-core/rollup.config.mjs @@ -0,0 +1,36 @@ +import { + createESMConfig, + createCJSConfig, + collectSourceEntries, +} from '../../scripts/build/rollup.base.mjs'; +import pkg from './package.json' with { type: 'json' }; + +const moduleInput = collectSourceEntries(); +const isESM = process.env.BUILD_FORMAT === 'esm'; +const isCJS = process.env.BUILD_FORMAT === 'cjs'; + +const configs = []; + +if (isESM || (!isESM && !isCJS)) { + configs.push( + createESMConfig({ + input: moduleInput, + pkg, + outputDir: 'dist/es', + preserveModules: true, + }) + ); +} + +if (isCJS || (!isESM && !isCJS)) { + configs.push( + createCJSConfig({ + input: moduleInput, + pkg, + outputDir: 'dist/cjs', + preserveModules: true, + }) + ); +} + +export default configs; diff --git a/packages/instantsearch-core/scripts/version.cjs b/packages/instantsearch-core/scripts/version.cjs new file mode 100755 index 00000000000..a7399a770ff --- /dev/null +++ b/packages/instantsearch-core/scripts/version.cjs @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const version = require('../package.json').version; + +fs.writeFileSync( + path.resolve(__dirname, '../src/version.ts'), + `export default '${version}';\n` +); diff --git a/packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx b/packages/instantsearch-core/src/__tests__/instantsearch-test.tsx similarity index 98% rename from packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx rename to packages/instantsearch-core/src/__tests__/instantsearch-test.tsx index de108bcc11e..d1a7023d221 100644 --- a/packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx +++ b/packages/instantsearch-core/src/__tests__/instantsearch-test.tsx @@ -14,23 +14,22 @@ import { wait } from '@instantsearch/testutils/wait'; import originalHelper from 'algoliasearch-helper'; import { h, render, createRef } from 'preact'; -import { createRenderOptions, createWidget } from '../../../test/createWidget'; -import { connectSearchBox, connectPagination } from '../../connectors'; -import { createInsightsMiddleware } from '../../middlewares'; -import { index } from '../../widgets'; -import InstantSearch from '../InstantSearch'; -import { noop, warning } from '../utils'; -import version from '../version'; +import { createRenderOptions, createWidget } from '../../test/createWidget'; +import { connectSearchBox, connectPagination } from '../connectors'; +import InstantSearch from '../instantsearch'; +import { noop, warning } from '../lib/utils'; +import { createInsightsMiddleware } from '../middlewares'; +import { index } from '../widgets'; import type { PaginationConnectorParams, PaginationWidgetDescription, -} from '../../connectors/pagination/connectPagination'; +} from '../connectors'; import type { SearchBoxWidgetDescription, SearchBoxConnectorParams, -} from '../../connectors/search-box/connectSearchBox'; -import type { UiState, Widget, IndexWidget } from '../../types'; +} from '../connectors'; +import type { UiState, Widget, IndexWidget } from '../types'; import type { RefObject } from 'preact'; type SearchBoxWidgetInstance = Widget< @@ -375,22 +374,6 @@ For more information, visit https://www.algolia.com/doc/guides/getting-insights- }); describe('InstantSearch', () => { - it('calls addAlgoliaAgent', () => { - const searchClient = createSearchClient({ - addAlgoliaAgent: jest.fn(), - }); - - // eslint-disable-next-line no-new - new InstantSearch({ - indexName: 'indexName', - searchClient, - }); - - expect(searchClient.addAlgoliaAgent).toHaveBeenCalledTimes(1); - expect(searchClient.addAlgoliaAgent).toHaveBeenCalledWith( - `instantsearch.js (${version})` - ); - }); it('does not call algoliasearchHelper', () => { // eslint-disable-next-line no-new diff --git a/packages/instantsearch-core/src/connectors/answers/connectAnswers.ts b/packages/instantsearch-core/src/connectors/answers/connectAnswers.ts new file mode 100644 index 00000000000..47bdadef0b8 --- /dev/null +++ b/packages/instantsearch-core/src/connectors/answers/connectAnswers.ts @@ -0,0 +1,267 @@ +import { + checkRendering, + createDocumentationMessageGenerator, + createConcurrentSafePromise, + addQueryID, + debounce, + addAbsolutePosition, + noop, + escapeHits, +} from '../../lib/utils'; + +import type { DebouncedFunction } from '../../lib/utils'; +import type { + Connector, + Hit, + FindAnswersOptions, + FindAnswersResponse, + WidgetRenderState, + FindAnswers, +} from '../../types'; + +type IndexWithAnswers = { + readonly findAnswers: any; +}; + +function hasFindAnswersMethod( + answersIndex: IndexWithAnswers | any +): answersIndex is IndexWithAnswers { + return typeof (answersIndex as IndexWithAnswers).findAnswers === 'function'; +} + +const withUsage = createDocumentationMessageGenerator({ + name: 'answers', + connector: true, +}); + +export type AnswersRenderState = { + /** + * The matched hits from Algolia API. + */ + hits: Hit[]; + + /** + * Whether it's still loading the results from the Answers API. + */ + isLoading: boolean; +}; + +export type AnswersConnectorParams = { + /** + * Attributes to use for predictions. + * If empty, we use all `searchableAttributes` to find answers. + * All your `attributesForPrediction` must be part of your `searchableAttributes`. + */ + attributesForPrediction?: string[]; + + /** + * The languages in the query. Currently only supports `en`. + */ + queryLanguages: ['en']; + + /** + * Maximum number of answers to retrieve from the Answers Engine. + * Cannot be greater than 1000. + * @default 1 + */ + nbHits?: number; + + /** + * Debounce time in milliseconds to debounce render + * @default 100 + */ + renderDebounceTime?: number; + + /** + * Debounce time in milliseconds to debounce search + * @default 100 + */ + searchDebounceTime?: number; + + /** + * Whether to escape HTML tags from hits string values. + * + * @default true + */ + escapeHTML?: boolean; + + /** + * Extra parameters to pass to findAnswers method. + * @default {} + */ + extraParameters?: FindAnswersOptions; +}; + +export type AnswersWidgetDescription = { + $$type: 'ais.answers'; + renderState: AnswersRenderState; + indexRenderState: { + answers: WidgetRenderState; + }; +}; + +export type AnswersConnector = Connector< + AnswersWidgetDescription, + AnswersConnectorParams +>; + +/** + * @deprecated the answers service is no longer offered, and this widget will be removed in InstantSearch.js v5 + */ +const connectAnswers: AnswersConnector = function connectAnswers( + renderFn, + unmountFn = noop +) { + checkRendering(renderFn, withUsage()); + + return (widgetParams) => { + const { + queryLanguages, + attributesForPrediction, + nbHits = 1, + renderDebounceTime = 100, + searchDebounceTime = 100, + // @MAJOR: this can default to false + escapeHTML = true, + extraParameters = {}, + } = widgetParams || {}; + + // @ts-expect-error checking for the wrong value + if (!queryLanguages || queryLanguages.length === 0) { + throw new Error( + withUsage('The `queryLanguages` expects an array of strings.') + ); + } + + const runConcurrentSafePromise = + createConcurrentSafePromise>(); + + let lastHits: FindAnswersResponse['hits'] = []; + let isLoading = false; + const debouncedRender = debounce(renderFn, renderDebounceTime); + + let debouncedRefine: DebouncedFunction; + + return { + $$type: 'ais.answers', + + init(initOptions) { + const { state, instantSearchInstance } = initOptions; + if (typeof instantSearchInstance.client.initIndex !== 'function') { + throw new Error(withUsage('`algoliasearch` <5 required.')); + } + const answersIndex = (instantSearchInstance.client.initIndex as any)( + state.index + ); + if (!hasFindAnswersMethod(answersIndex)) { + throw new Error(withUsage('`algoliasearch` >= 4.8.0 required.')); + } + debouncedRefine = debounce( + answersIndex.findAnswers as unknown as FindAnswers, + searchDebounceTime + ); + + renderFn( + { + ...this.getWidgetRenderState(initOptions), + instantSearchInstance: initOptions.instantSearchInstance, + }, + true + ); + }, + + render(renderOptions) { + const query = renderOptions.state.query; + if (!query) { + // renders nothing with empty query + lastHits = []; + isLoading = false; + renderFn( + { + ...this.getWidgetRenderState(renderOptions), + instantSearchInstance: renderOptions.instantSearchInstance, + }, + false + ); + return; + } + + // render the loader + lastHits = []; + isLoading = true; + renderFn( + { + ...this.getWidgetRenderState(renderOptions), + instantSearchInstance: renderOptions.instantSearchInstance, + }, + false + ); + + // call /answers API + runConcurrentSafePromise( + debouncedRefine(query, queryLanguages, { + ...extraParameters, + nbHits, + attributesForPrediction, + }) as unknown as Promise> + ).then((result) => { + if (!result) { + // It's undefined when it's debounced. + return; + } + + if (escapeHTML && result.hits.length > 0) { + result.hits = escapeHits(result.hits); + } + + const hitsWithAbsolutePosition = addAbsolutePosition( + result.hits, + 0, + nbHits + ); + + const hitsWithAbsolutePositionAndQueryID = addQueryID( + hitsWithAbsolutePosition, + result.queryID + ); + + lastHits = hitsWithAbsolutePositionAndQueryID; + isLoading = false; + debouncedRender( + { + ...this.getWidgetRenderState(renderOptions), + instantSearchInstance: renderOptions.instantSearchInstance, + }, + false + ); + }); + }, + + getRenderState(renderState, renderOptions) { + return { + ...renderState, + answers: this.getWidgetRenderState(renderOptions), + }; + }, + + getWidgetRenderState() { + return { + hits: lastHits, + isLoading, + widgetParams, + }; + }, + + dispose({ state }) { + unmountFn(); + return state; + }, + + getWidgetSearchParameters(state) { + return state; + }, + }; + }; +}; + +export default connectAnswers; diff --git a/packages/instantsearch.js/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts b/packages/instantsearch-core/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts rename to packages/instantsearch-core/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts index 520a29cc67e..41ca1953926 100644 --- a/packages/instantsearch.js/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts +++ b/packages/instantsearch-core/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts @@ -18,7 +18,7 @@ import { createRenderOptions, createDisposeOptions, } from '../../../../test/createWidget'; -import instantsearch from '../../../index.es'; +import InstantSearch from '../../../instantsearch'; import { TAG_PLACEHOLDER } from '../../../lib/utils'; import connectAutocomplete from '../connectAutocomplete'; @@ -887,7 +887,7 @@ search.addWidgets([ }, }); - const instantSearchInstance = instantsearch({ + const instantSearchInstance = new InstantSearch({ searchClient, stalledSearchDelay: 1, indexName: 'indexName', diff --git a/packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts b/packages/instantsearch-core/src/connectors/autocomplete/connectAutocomplete.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts rename to packages/instantsearch-core/src/connectors/autocomplete/connectAutocomplete.ts diff --git a/packages/instantsearch.js/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.ts b/packages/instantsearch-core/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.ts rename to packages/instantsearch-core/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.ts diff --git a/packages/instantsearch.js/src/connectors/breadcrumb/connectBreadcrumb.ts b/packages/instantsearch-core/src/connectors/breadcrumb/connectBreadcrumb.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/breadcrumb/connectBreadcrumb.ts rename to packages/instantsearch-core/src/connectors/breadcrumb/connectBreadcrumb.ts diff --git a/packages/instantsearch.js/src/connectors/chat/__tests__/connectChat-test.ts b/packages/instantsearch-core/src/connectors/chat/__tests__/connectChat-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/chat/__tests__/connectChat-test.ts rename to packages/instantsearch-core/src/connectors/chat/__tests__/connectChat-test.ts index 11d0feb6e3c..a2ee7630d7f 100644 --- a/packages/instantsearch.js/src/connectors/chat/__tests__/connectChat-test.ts +++ b/packages/instantsearch-core/src/connectors/chat/__tests__/connectChat-test.ts @@ -5,22 +5,24 @@ import { createSearchClient } from '@instantsearch/mocks'; import { waitFor } from '@testing-library/dom'; import algoliasearchHelper from 'algoliasearch-helper'; +import { Chat } from 'instantsearch-core'; import { createInstantSearch } from '../../../../test/createInstantSearch'; import { createInitOptions, createRenderOptions, } from '../../../../test/createWidget'; -import { Chat } from '../../../lib/chat'; import connectChat from '../connectChat'; -import type { UIMessage, ChatTransport } from '../../../lib/ai-lite'; import type { InstantSearch, IndexWidget } from '../../../types'; import type { ChatConnectorParams } from '../connectChat'; +import type { UIMessage, ChatTransport } from 'instantsearch-core'; -jest.mock('../../../lib/utils/sendChatMessageFeedback', () => ({ - sendChatMessageFeedback: jest.fn(() => Promise.resolve(new Response('{}'))), -})); +const fetchMock = jest.fn(() => Promise.resolve(new Response('{}'))); +Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + writable: true, +}); describe('connectChat', () => { const getInitializedWidget = (widgetParams: ChatConnectorParams = {}) => { @@ -533,10 +535,7 @@ describe('connectChat', () => { }); it('prevents double voting on the same message', () => { - const { sendChatMessageFeedback: mockedFn } = jest.requireMock( - '../../../lib/utils/sendChatMessageFeedback' - ); - mockedFn.mockClear(); + fetchMock.mockClear(); const { getRenderState } = getInitializedWidget({ agentId: 'agentId', @@ -547,7 +546,7 @@ describe('connectChat', () => { renderState.sendChatMessageFeedback!('msg-1', 1); renderState.sendChatMessageFeedback!('msg-1', 0); - expect(mockedFn).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/instantsearch.js/src/connectors/chat/connectChat.ts b/packages/instantsearch-core/src/connectors/chat/connectChat.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/chat/connectChat.ts rename to packages/instantsearch-core/src/connectors/chat/connectChat.ts index 9bcb4aa458b..75d58eb66ad 100644 --- a/packages/instantsearch.js/src/connectors/chat/connectChat.ts +++ b/packages/instantsearch-core/src/connectors/chat/connectChat.ts @@ -36,13 +36,17 @@ import type { WidgetRenderState, IndexRenderState, } from '../../types'; -import type { AlgoliaSearchHelper, SearchResults } from 'algoliasearch-helper'; import type { AddToolResultWithOutput, - UserClientSideTool, - ClientSideTools, + ApplyFiltersParams, ClientSideTool, -} from 'instantsearch-ui-components'; + ClientSideTools, + UserClientSideTool, +} from './types'; +import type { AlgoliaSearchHelper, SearchResults } from 'algoliasearch-helper'; + + +export type * from './types'; const withUsage = createDocumentationMessageGenerator({ name: 'chat', @@ -160,11 +164,6 @@ export type ChatCustomInstance = { requestOptions?: never; }; -export type ApplyFiltersParams = { - query?: string; - facetFilters?: string[][]; -}; - export type ChatInit = ChatInitWithoutTransport & ChatTransport; diff --git a/packages/instantsearch.js/src/connectors/chat/connectChatTrigger.ts b/packages/instantsearch-core/src/connectors/chat/connectChatTrigger.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/chat/connectChatTrigger.ts rename to packages/instantsearch-core/src/connectors/chat/connectChatTrigger.ts diff --git a/packages/instantsearch-core/src/connectors/chat/types.ts b/packages/instantsearch-core/src/connectors/chat/types.ts new file mode 100644 index 00000000000..08a1aa5522b --- /dev/null +++ b/packages/instantsearch-core/src/connectors/chat/types.ts @@ -0,0 +1,77 @@ +import type { + AbstractChat, + ChatInit as ChatInitAi, + UIMessage, +} from '../../lib/chat'; +import type { SendEventForHits } from '../../lib/utils/createSendEventForHits'; +import type { SearchParameters } from 'algoliasearch-helper'; + +type ChatToolMessage = Extract< + UIMessage['parts'][number], + { type: `tool-${string}` } +>; + +export type AddToolResult = AbstractChat['addToolResult']; + +export type AddToolResultWithOutput = ( + params: Pick[0], 'output'> +) => ReturnType; + +type SearchToolInputBase = { + query: string; + number_of_results?: number; +}; + +type DefaultSearchToolInput = SearchToolInputBase & { + facet_filters?: string[][]; +}; + +type McpSearchToolInput = SearchToolInputBase & { + facet_filters?: undefined; + [facetKey: `facet_${string}`]: string[] | undefined; +}; + +export type SearchToolInput = DefaultSearchToolInput | McpSearchToolInput; + +export type ApplyFiltersParams = { + query?: string; + facetFilters?: string[][]; +}; + +export type ClientSideToolComponentProps = { + message: ChatToolMessage; + messages?: UIMessage[]; + indexUiState: object; + setIndexUiState: (state: object) => void; + onClose: () => void; + addToolResult: AddToolResultWithOutput; + applyFilters: (params: ApplyFiltersParams) => SearchParameters; + sendEvent: SendEventForHits; +}; + +export type ClientSideToolComponent = ( + props: ClientSideToolComponentProps +) => any; + +export type ClientSideTool = { + layoutComponent?: ClientSideToolComponent; + streamInput?: boolean; + addToolResult: AddToolResult; + sendEvent?: SendEventForHits; + onToolCall?: ( + params: Parameters< + NonNullable['onToolCall']> + >[0]['toolCall'] & { + addToolResult: AddToolResultWithOutput; + } + ) => void; + applyFilters: (params: ApplyFiltersParams) => SearchParameters; +}; + +export type ClientSideTools = Record; + +export type UserClientSideTool = Omit< + ClientSideTool, + 'addToolResult' | 'applyFilters' | 'sendEvent' +>; +export type UserClientSideTools = Record; diff --git a/packages/instantsearch.js/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.ts b/packages/instantsearch-core/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.ts rename to packages/instantsearch-core/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.ts diff --git a/packages/instantsearch.js/src/connectors/clear-refinements/connectClearRefinements.ts b/packages/instantsearch-core/src/connectors/clear-refinements/connectClearRefinements.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/clear-refinements/connectClearRefinements.ts rename to packages/instantsearch-core/src/connectors/clear-refinements/connectClearRefinements.ts diff --git a/packages/instantsearch.js/src/connectors/configure/__tests__/connectConfigure-test.ts b/packages/instantsearch-core/src/connectors/configure/__tests__/connectConfigure-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/configure/__tests__/connectConfigure-test.ts rename to packages/instantsearch-core/src/connectors/configure/__tests__/connectConfigure-test.ts diff --git a/packages/instantsearch.js/src/connectors/configure/connectConfigure.ts b/packages/instantsearch-core/src/connectors/configure/connectConfigure.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/configure/connectConfigure.ts rename to packages/instantsearch-core/src/connectors/configure/connectConfigure.ts diff --git a/packages/instantsearch.js/src/connectors/current-refinements/__tests__/connectCurrentRefinements-test.ts b/packages/instantsearch-core/src/connectors/current-refinements/__tests__/connectCurrentRefinements-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/current-refinements/__tests__/connectCurrentRefinements-test.ts rename to packages/instantsearch-core/src/connectors/current-refinements/__tests__/connectCurrentRefinements-test.ts diff --git a/packages/instantsearch.js/src/connectors/current-refinements/connectCurrentRefinements.ts b/packages/instantsearch-core/src/connectors/current-refinements/connectCurrentRefinements.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/current-refinements/connectCurrentRefinements.ts rename to packages/instantsearch-core/src/connectors/current-refinements/connectCurrentRefinements.ts diff --git a/packages/instantsearch.js/src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts b/packages/instantsearch-core/src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts rename to packages/instantsearch-core/src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts index 69616995a20..6f1e1b8cbed 100644 --- a/packages/instantsearch.js/src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts +++ b/packages/instantsearch-core/src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts @@ -24,8 +24,8 @@ import { index } from '../../../widgets'; import connectHierarchicalMenu from '../../hierarchical-menu/connectHierarchicalMenu'; import connectRefinementList from '../../refinement-list/connectRefinementList'; -import type { SearchResponse } from '../../../types/algoliasearch'; import type { DynamicWidgetsConnectorParams } from '../connectDynamicWidgets'; +import type { SearchResponse } from 'instantsearch-core'; expect.addSnapshotSerializer(widgetSnapshotSerializer); diff --git a/packages/instantsearch.js/src/connectors/dynamic-widgets/connectDynamicWidgets.ts b/packages/instantsearch-core/src/connectors/dynamic-widgets/connectDynamicWidgets.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/dynamic-widgets/connectDynamicWidgets.ts rename to packages/instantsearch-core/src/connectors/dynamic-widgets/connectDynamicWidgets.ts diff --git a/packages/instantsearch-core/src/connectors/feeds/FeedContainer.ts b/packages/instantsearch-core/src/connectors/feeds/FeedContainer.ts new file mode 100644 index 00000000000..828ef23e69d --- /dev/null +++ b/packages/instantsearch-core/src/connectors/feeds/FeedContainer.ts @@ -0,0 +1,310 @@ +import algoliasearchHelper from 'algoliasearch-helper'; + +import { + createInitArgs, + createRenderArgs, + storeRenderState, +} from '../../lib/utils'; + +import type { + InstantSearch, + UiState, + IndexUiState, + Widget, + IndexWidget, + DisposeOptions, + RenderOptions, +} from '../../types'; +import type { SearchParameters } from 'algoliasearch-helper'; + +export function createFeedContainer( + feedID: string, + parentIndex: IndexWidget, + instantSearchInstance: InstantSearch +): IndexWidget { + let localWidgets: Array = []; + let initialized = false; + + const container: IndexWidget = { + $$type: 'ais.feedContainer', + $$widgetType: 'ais.feedContainer', + _isolated: true, + + getIndexName: () => parentIndex.getIndexName(), + getIndexId: () => feedID, + getHelper: () => parentIndex.getHelper(), + + getResults() { + const parentResults = parentIndex.getResults(); + if (!parentResults) return null; + if (!parentResults.feeds) { + // Single-feed backward compat: no feeds array means the parent result + // itself is the only feed. + if (feedID === '') { + parentResults._state = parentIndex.getHelper()!.state; + return parentResults; + } + return null; + } + const feed = parentResults.feeds.find((f) => f.feedID === feedID); + if (!feed) return null; + // Optimistic state patching — same as index widget (index.ts:365-370) + feed._state = parentIndex.getHelper()!.state; + return feed; + }, + + getResultsForWidget() { + return this.getResults(); + }, + + getParent: () => parentIndex, + getWidgets: () => localWidgets, + getScopedResults: () => parentIndex.getScopedResults(), + getPreviousState: () => null, + createURL: ( + nextState: SearchParameters | ((state: IndexUiState) => IndexUiState) + ) => parentIndex.createURL(nextState), + scheduleLocalSearch: () => parentIndex.scheduleLocalSearch(), + + addWidgets(widgets) { + const flatWidgets = widgets.reduce>( + (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), + [] + ); + flatWidgets.forEach((widget) => { + widget.parent = container; + }); + localWidgets = localWidgets.concat(flatWidgets); + + if (initialized) { + flatWidgets.forEach((widget) => { + if (widget.getRenderState) { + const renderState = widget.getRenderState( + instantSearchInstance.renderState[container.getIndexId()] || {}, + createInitArgs( + instantSearchInstance, + container, + instantSearchInstance._initialUiState + ) + ); + storeRenderState({ + renderState, + instantSearchInstance, + parent: container, + }); + } + }); + + flatWidgets.forEach((widget) => { + if (widget.init) { + widget.init( + createInitArgs( + instantSearchInstance, + container, + instantSearchInstance._initialUiState + ) + ); + } + }); + + // Merge children's search params (e.g. disjunctiveFacets) into the + // parent's helper state so they're included in the composition request. + // uiState is {} because URL-derived refinements are already on the + // parent state; children only need to declare structural params. + const parentHelper = parentIndex.getHelper()!; + const withChildParams = container.getWidgetSearchParameters( + parentHelper.state, + { uiState: {} } + ); + if (withChildParams !== parentHelper.state) { + parentHelper.setState(withChildParams); + } + } + + return container; + }, + + removeWidgets(widgets) { + const flatWidgets = widgets.reduce>( + (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), + [] + ); + const helper = parentIndex.getHelper(); + + if (!helper) { + localWidgets = localWidgets.filter((w) => !flatWidgets.includes(w)); + return container; + } + + // Chain through children's dispose so widgets clean up the + // SearchParameters they declared (e.g. RefinementList removes its + // disjunctiveFacet) instead of leaving them stale on the parent helper. + let cleanedState: SearchParameters = helper.state; + + flatWidgets.forEach((widget) => { + if (widget.dispose) { + const next = widget.dispose({ + helper, + state: cleanedState, + recommendState: helper.recommendState, + parent: container, + }); + + if (next instanceof algoliasearchHelper.RecommendParameters) { + // ignore — FeedContainer doesn't manage recommend state + } else if (next) { + cleanedState = next; + } + } + }); + + localWidgets = localWidgets.filter((w) => !flatWidgets.includes(w)); + + if (cleanedState !== helper.state) { + helper.setState(cleanedState); + } + + return container; + }, + + init() { + initialized = true; + + localWidgets.forEach((widget) => { + if (widget.getRenderState) { + const renderState = widget.getRenderState( + instantSearchInstance.renderState[container.getIndexId()] || {}, + createInitArgs( + instantSearchInstance, + container, + instantSearchInstance._initialUiState + ) + ); + storeRenderState({ + renderState, + instantSearchInstance, + parent: container, + }); + } + }); + + localWidgets.forEach((widget) => { + if (widget.init) { + widget.init( + createInitArgs( + instantSearchInstance, + container, + instantSearchInstance._initialUiState + ) + ); + } + }); + }, + + render() { + localWidgets.forEach((widget) => { + if (widget.getRenderState) { + const renderState = widget.getRenderState( + instantSearchInstance.renderState[container.getIndexId()] || {}, + createRenderArgs( + instantSearchInstance, + container, + widget + ) as RenderOptions + ); + storeRenderState({ + renderState, + instantSearchInstance, + parent: container, + }); + } + }); + + localWidgets.forEach((widget) => { + if (widget.render) { + widget.render( + createRenderArgs( + instantSearchInstance, + container, + widget + ) as RenderOptions + ); + } + }); + }, + + dispose(disposeOptions?: DisposeOptions) { + const helper = parentIndex.getHelper(); + + // Chain through children's dispose to return a cleaned state + // (e.g. RefinementList.dispose removes its disjunctiveFacet declaration). + // This mirrors how the index widget's removeWidgets chains dispose calls. + let cleanedState = disposeOptions?.state ?? helper?.state; + + localWidgets.forEach((widget) => { + if (widget.dispose && helper) { + const next = widget.dispose({ + helper, + state: cleanedState!, + recommendState: helper.recommendState, + parent: container, + }); + + if (next instanceof algoliasearchHelper.RecommendParameters) { + // ignore — FeedContainer doesn't manage recommend state + } else if (next) { + cleanedState = next; + } + } + }); + + localWidgets = []; + initialized = false; + return cleanedState; + }, + + getWidgetState(uiState: UiState) { + return this.getWidgetUiState(uiState); + }, + + getWidgetUiState( + uiState: TUiState + ): TUiState { + const helper = parentIndex.getHelper()!; + const widgetUiStateOptions = { + searchParameters: helper.state, + helper, + }; + return localWidgets.reduce( + (state, widget) => + widget.getWidgetUiState + ? (widget.getWidgetUiState(state, widgetUiStateOptions) as TUiState) + : state, + uiState + ); + }, + + getWidgetSearchParameters( + searchParameters: SearchParameters, + { uiState }: { uiState: IndexUiState } + ) { + return localWidgets.reduce( + (params, widget) => + widget.getWidgetSearchParameters + ? widget.getWidgetSearchParameters(params, { uiState }) + : params, + searchParameters + ); + }, + + refreshUiState() { + // no-op: FeedContainer doesn't own UI state + }, + + setIndexUiState() { + // no-op: FeedContainer delegates to parent + }, + }; + + return container; +} diff --git a/packages/instantsearch.js/src/connectors/feeds/__tests__/FeedContainer-test.ts b/packages/instantsearch-core/src/connectors/feeds/__tests__/FeedContainer-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/feeds/__tests__/FeedContainer-test.ts rename to packages/instantsearch-core/src/connectors/feeds/__tests__/FeedContainer-test.ts diff --git a/packages/instantsearch.js/src/connectors/feeds/__tests__/connectFeeds-test.ts b/packages/instantsearch-core/src/connectors/feeds/__tests__/connectFeeds-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/feeds/__tests__/connectFeeds-test.ts rename to packages/instantsearch-core/src/connectors/feeds/__tests__/connectFeeds-test.ts diff --git a/packages/instantsearch.js/src/connectors/feeds/connectFeeds.ts b/packages/instantsearch-core/src/connectors/feeds/connectFeeds.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/feeds/connectFeeds.ts rename to packages/instantsearch-core/src/connectors/feeds/connectFeeds.ts diff --git a/packages/instantsearch.js/src/connectors/filter-suggestions/connectFilterSuggestions.ts b/packages/instantsearch-core/src/connectors/filter-suggestions/connectFilterSuggestions.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/filter-suggestions/connectFilterSuggestions.ts rename to packages/instantsearch-core/src/connectors/filter-suggestions/connectFilterSuggestions.ts diff --git a/packages/instantsearch.js/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts b/packages/instantsearch-core/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts rename to packages/instantsearch-core/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts diff --git a/packages/instantsearch.js/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts b/packages/instantsearch-core/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts rename to packages/instantsearch-core/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts diff --git a/packages/instantsearch.js/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts b/packages/instantsearch-core/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts rename to packages/instantsearch-core/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts index aebf136d49a..f2e7db0d1dc 100644 --- a/packages/instantsearch.js/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts +++ b/packages/instantsearch-core/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts @@ -19,7 +19,7 @@ import { createInitOptions, createRenderOptions, } from '../../../../test/createWidget'; -import instantsearch from '../../../index.es'; +import InstantSearch from '../../../instantsearch'; import connectGeoSearch from '../connectGeoSearch'; import type { SearchResponse } from '../../../types'; @@ -1762,7 +1762,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - const instantSearchInstance = instantsearch({ + const instantSearchInstance = new InstantSearch({ searchClient, stalledSearchDelay: 1, indexName: 'indexName', diff --git a/packages/instantsearch.js/src/connectors/geo-search/connectGeoSearch.ts b/packages/instantsearch-core/src/connectors/geo-search/connectGeoSearch.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/geo-search/connectGeoSearch.ts rename to packages/instantsearch-core/src/connectors/geo-search/connectGeoSearch.ts diff --git a/packages/instantsearch.js/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts b/packages/instantsearch-core/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts rename to packages/instantsearch-core/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts diff --git a/packages/instantsearch.js/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts b/packages/instantsearch-core/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts rename to packages/instantsearch-core/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts diff --git a/packages/instantsearch.js/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.ts b/packages/instantsearch-core/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.ts rename to packages/instantsearch-core/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.ts diff --git a/packages/instantsearch.js/src/connectors/hits-per-page/connectHitsPerPage.ts b/packages/instantsearch-core/src/connectors/hits-per-page/connectHitsPerPage.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/hits-per-page/connectHitsPerPage.ts rename to packages/instantsearch-core/src/connectors/hits-per-page/connectHitsPerPage.ts diff --git a/packages/instantsearch.js/src/connectors/hits/__tests__/connectHits-test.ts b/packages/instantsearch-core/src/connectors/hits/__tests__/connectHits-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/hits/__tests__/connectHits-test.ts rename to packages/instantsearch-core/src/connectors/hits/__tests__/connectHits-test.ts index 6a2a549d729..68b02b946e3 100644 --- a/packages/instantsearch.js/src/connectors/hits/__tests__/connectHits-test.ts +++ b/packages/instantsearch-core/src/connectors/hits/__tests__/connectHits-test.ts @@ -19,7 +19,7 @@ import { createInitOptions, createRenderOptions, } from '../../../../test/createWidget'; -import instantsearch from '../../../index.es'; +import InstantSearch from '../../../instantsearch'; import { TAG_PLACEHOLDER, deserializePayload } from '../../../lib/utils'; import connectHits from '../connectHits'; @@ -886,7 +886,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }, }); - const instantSearchInstance = instantsearch({ + const instantSearchInstance = new InstantSearch({ searchClient, stalledSearchDelay: 1, indexName: 'indexName', diff --git a/packages/instantsearch.js/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts b/packages/instantsearch-core/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts rename to packages/instantsearch-core/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts diff --git a/packages/instantsearch.js/src/connectors/hits/connectHits.ts b/packages/instantsearch-core/src/connectors/hits/connectHits.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/hits/connectHits.ts rename to packages/instantsearch-core/src/connectors/hits/connectHits.ts diff --git a/packages/instantsearch.js/src/connectors/hits/connectHitsWithInsights.ts b/packages/instantsearch-core/src/connectors/hits/connectHitsWithInsights.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/hits/connectHitsWithInsights.ts rename to packages/instantsearch-core/src/connectors/hits/connectHitsWithInsights.ts diff --git a/packages/instantsearch-core/src/connectors/index.ts b/packages/instantsearch-core/src/connectors/index.ts new file mode 100644 index 00000000000..a1d7b5d441d --- /dev/null +++ b/packages/instantsearch-core/src/connectors/index.ts @@ -0,0 +1,74 @@ +export { default as connectAutocomplete } from './autocomplete/connectAutocomplete'; +export type * from './autocomplete/connectAutocomplete'; +export { default as connectBreadcrumb } from './breadcrumb/connectBreadcrumb'; +export type * from './breadcrumb/connectBreadcrumb'; +export { default as connectChat } from './chat/connectChat'; +export type * from './chat/types'; +export type * from './chat/connectChat'; +export { default as connectChatTrigger } from './chat/connectChatTrigger'; +export type * from './chat/connectChatTrigger'; +export { default as connectClearRefinements } from './clear-refinements/connectClearRefinements'; +export type * from './clear-refinements/connectClearRefinements'; +export { default as connectConfigure } from './configure/connectConfigure'; +export type * from './configure/connectConfigure'; +export { default as connectCurrentRefinements } from './current-refinements/connectCurrentRefinements'; +export type * from './current-refinements/connectCurrentRefinements'; +export { default as connectDynamicWidgets } from './dynamic-widgets/connectDynamicWidgets'; +export type * from './dynamic-widgets/connectDynamicWidgets'; +export { default as connectFeeds } from './feeds/connectFeeds'; +export type * from './feeds/connectFeeds'; +export { default as connectFilterSuggestions } from './filter-suggestions/connectFilterSuggestions'; +export type * from './filter-suggestions/connectFilterSuggestions'; +export { default as connectFrequentlyBoughtTogether } from './frequently-bought-together/connectFrequentlyBoughtTogether'; +export type * from './frequently-bought-together/connectFrequentlyBoughtTogether'; +export { default as connectGeoSearch } from './geo-search/connectGeoSearch'; +export type * from './geo-search/connectGeoSearch'; +export { default as connectHierarchicalMenu } from './hierarchical-menu/connectHierarchicalMenu'; +export type * from './hierarchical-menu/connectHierarchicalMenu'; +export { default as connectHits } from './hits/connectHits'; +export type * from './hits/connectHits'; +export { default as connectHitsWithInsights } from './hits/connectHitsWithInsights'; +export type * from './hits/connectHitsWithInsights'; +export { default as connectHitsPerPage } from './hits-per-page/connectHitsPerPage'; +export type * from './hits-per-page/connectHitsPerPage'; +export { default as connectInfiniteHits } from './infinite-hits/connectInfiniteHits'; +export type * from './infinite-hits/connectInfiniteHits'; +export { default as connectInfiniteHitsWithInsights } from './infinite-hits/connectInfiniteHitsWithInsights'; +export type * from './infinite-hits/connectInfiniteHitsWithInsights'; +export { default as connectLookingSimilar } from './looking-similar/connectLookingSimilar'; +export type * from './looking-similar/connectLookingSimilar'; +export { default as connectMenu } from './menu/connectMenu'; +export type * from './menu/connectMenu'; +export { default as connectNumericMenu } from './numeric-menu/connectNumericMenu'; +export type * from './numeric-menu/connectNumericMenu'; +export { default as connectPagination } from './pagination/connectPagination'; +export type * from './pagination/connectPagination'; +export { default as connectPoweredBy } from './powered-by/connectPoweredBy'; +export type * from './powered-by/connectPoweredBy'; +export { default as connectQueryRules } from './query-rules/connectQueryRules'; +export type * from './query-rules/connectQueryRules'; +export { default as connectRange } from './range/connectRange'; +export type * from './range/connectRange'; +export { default as connectRatingMenu } from './rating-menu/connectRatingMenu'; +export type * from './rating-menu/connectRatingMenu'; +export { default as connectRefinementList } from './refinement-list/connectRefinementList'; +export type * from './refinement-list/connectRefinementList'; +export { default as connectRelatedProducts } from './related-products/connectRelatedProducts'; +export type * from './related-products/connectRelatedProducts'; +export { default as connectRelevantSort } from './relevant-sort/connectRelevantSort'; +export type * from './relevant-sort/connectRelevantSort'; +export { default as connectSearchBox } from './search-box/connectSearchBox'; +export type * from './search-box/connectSearchBox'; +export { default as connectSortBy } from './sort-by/connectSortBy'; +export type * from './sort-by/connectSortBy'; +export { default as connectStats } from './stats/connectStats'; +export type * from './stats/connectStats'; +export { default as connectToggleRefinement } from './toggle-refinement/connectToggleRefinement'; +export type * from './toggle-refinement/connectToggleRefinement'; +export { default as connectTrendingFacets } from './trending-facets/connectTrendingFacets'; +export type * from './trending-facets/connectTrendingFacets'; +export { default as connectTrendingItems } from './trending-items/connectTrendingItems'; +export type * from './trending-items/connectTrendingItems'; +export { default as connectVoiceSearch } from './voice-search/connectVoiceSearch'; +export type * from './voice-search/connectVoiceSearch'; +export * from './feeds/FeedContainer'; diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts b/packages/instantsearch-core/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts rename to packages/instantsearch-core/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts index b6c5420832f..ece44c0040c 100644 --- a/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts +++ b/packages/instantsearch-core/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts @@ -16,7 +16,7 @@ import { createInitOptions, createRenderOptions, } from '../../../../test/createWidget'; -import instantsearch from '../../../index.es'; +import InstantSearch from '../../../instantsearch'; import { createInfiniteHitsSessionStorageCache } from '../../../lib/infiniteHitsCache'; import { TAG_PLACEHOLDER, deserializePayload } from '../../../lib/utils'; import connectInfiniteHits from '../connectInfiniteHits'; @@ -1807,7 +1807,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }, }); - const instantSearchInstance = instantsearch({ + const instantSearchInstance = new InstantSearch({ searchClient, stalledSearchDelay: 1, indexName: 'indexName', diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts b/packages/instantsearch-core/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts rename to packages/instantsearch-core/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts b/packages/instantsearch-core/src/connectors/infinite-hits/connectInfiniteHits.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts rename to packages/instantsearch-core/src/connectors/infinite-hits/connectInfiniteHits.ts diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHitsWithInsights.ts b/packages/instantsearch-core/src/connectors/infinite-hits/connectInfiniteHitsWithInsights.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHitsWithInsights.ts rename to packages/instantsearch-core/src/connectors/infinite-hits/connectInfiniteHitsWithInsights.ts diff --git a/packages/instantsearch.js/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts b/packages/instantsearch-core/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts rename to packages/instantsearch-core/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts diff --git a/packages/instantsearch.js/src/connectors/looking-similar/connectLookingSimilar.ts b/packages/instantsearch-core/src/connectors/looking-similar/connectLookingSimilar.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/looking-similar/connectLookingSimilar.ts rename to packages/instantsearch-core/src/connectors/looking-similar/connectLookingSimilar.ts diff --git a/packages/instantsearch.js/src/connectors/menu/__tests__/connectMenu-test.ts b/packages/instantsearch-core/src/connectors/menu/__tests__/connectMenu-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/menu/__tests__/connectMenu-test.ts rename to packages/instantsearch-core/src/connectors/menu/__tests__/connectMenu-test.ts diff --git a/packages/instantsearch.js/src/connectors/menu/connectMenu.ts b/packages/instantsearch-core/src/connectors/menu/connectMenu.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/menu/connectMenu.ts rename to packages/instantsearch-core/src/connectors/menu/connectMenu.ts diff --git a/packages/instantsearch.js/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts b/packages/instantsearch-core/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts rename to packages/instantsearch-core/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts diff --git a/packages/instantsearch.js/src/connectors/numeric-menu/connectNumericMenu.ts b/packages/instantsearch-core/src/connectors/numeric-menu/connectNumericMenu.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/numeric-menu/connectNumericMenu.ts rename to packages/instantsearch-core/src/connectors/numeric-menu/connectNumericMenu.ts diff --git a/packages/instantsearch-core/src/connectors/pagination/Paginator.ts b/packages/instantsearch-core/src/connectors/pagination/Paginator.ts new file mode 100644 index 00000000000..283cfcae0df --- /dev/null +++ b/packages/instantsearch-core/src/connectors/pagination/Paginator.ts @@ -0,0 +1,72 @@ +import { range } from '../../lib/utils'; + +class Paginator { + public currentPage: number; + public total: number; + public padding: number; + + public constructor(params: { + currentPage: number; + total: number; + padding: number; + }) { + this.currentPage = params.currentPage; + this.total = params.total; + this.padding = params.padding; + } + + public pages() { + const { total, currentPage, padding } = this; + + if (total === 0) return [0]; + + const totalDisplayedPages = this.nbPagesDisplayed(padding, total); + if (totalDisplayedPages === total) { + return range({ end: total }); + } + + const paddingLeft = this.calculatePaddingLeft( + currentPage, + padding, + total, + totalDisplayedPages + ); + const paddingRight = totalDisplayedPages - paddingLeft; + + const first = currentPage - paddingLeft; + const last = currentPage + paddingRight; + + return range({ start: first, end: last }); + } + + public nbPagesDisplayed(padding: number, total: number) { + return Math.min(2 * padding + 1, total); + } + + public calculatePaddingLeft( + current: number, + padding: number, + total: number, + totalDisplayedPages: number + ) { + if (current <= padding) { + return current; + } + + if (current >= total - padding) { + return totalDisplayedPages - (total - current); + } + + return padding; + } + + public isLastPage() { + return this.currentPage >= this.total - 1; + } + + public isFirstPage() { + return this.currentPage <= 0; + } +} + +export default Paginator; diff --git a/packages/instantsearch.js/src/connectors/pagination/__tests__/Paginator-test.ts b/packages/instantsearch-core/src/connectors/pagination/__tests__/Paginator-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/pagination/__tests__/Paginator-test.ts rename to packages/instantsearch-core/src/connectors/pagination/__tests__/Paginator-test.ts diff --git a/packages/instantsearch.js/src/connectors/pagination/__tests__/connectPagination-test.ts b/packages/instantsearch-core/src/connectors/pagination/__tests__/connectPagination-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/pagination/__tests__/connectPagination-test.ts rename to packages/instantsearch-core/src/connectors/pagination/__tests__/connectPagination-test.ts diff --git a/packages/instantsearch.js/src/connectors/pagination/connectPagination.ts b/packages/instantsearch-core/src/connectors/pagination/connectPagination.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/pagination/connectPagination.ts rename to packages/instantsearch-core/src/connectors/pagination/connectPagination.ts diff --git a/packages/instantsearch.js/src/connectors/powered-by/__tests__/connectPoweredBy-test.ts b/packages/instantsearch-core/src/connectors/powered-by/__tests__/connectPoweredBy-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/powered-by/__tests__/connectPoweredBy-test.ts rename to packages/instantsearch-core/src/connectors/powered-by/__tests__/connectPoweredBy-test.ts diff --git a/packages/instantsearch.js/src/connectors/powered-by/connectPoweredBy.ts b/packages/instantsearch-core/src/connectors/powered-by/connectPoweredBy.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/powered-by/connectPoweredBy.ts rename to packages/instantsearch-core/src/connectors/powered-by/connectPoweredBy.ts diff --git a/packages/instantsearch.js/src/connectors/query-rules/__tests__/connectQueryRules-test.ts b/packages/instantsearch-core/src/connectors/query-rules/__tests__/connectQueryRules-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/query-rules/__tests__/connectQueryRules-test.ts rename to packages/instantsearch-core/src/connectors/query-rules/__tests__/connectQueryRules-test.ts diff --git a/packages/instantsearch.js/src/connectors/query-rules/connectQueryRules.ts b/packages/instantsearch-core/src/connectors/query-rules/connectQueryRules.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/query-rules/connectQueryRules.ts rename to packages/instantsearch-core/src/connectors/query-rules/connectQueryRules.ts diff --git a/packages/instantsearch.js/src/connectors/range/__tests__/connectRange-test.ts b/packages/instantsearch-core/src/connectors/range/__tests__/connectRange-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/range/__tests__/connectRange-test.ts rename to packages/instantsearch-core/src/connectors/range/__tests__/connectRange-test.ts index f2920ea542b..b6fb19c3109 100644 --- a/packages/instantsearch.js/src/connectors/range/__tests__/connectRange-test.ts +++ b/packages/instantsearch-core/src/connectors/range/__tests__/connectRange-test.ts @@ -11,13 +11,13 @@ import jsHelper, { SearchParameters, } from 'algoliasearch-helper'; -import instantsearch from '../../..'; import { createInstantSearch } from '../../../../test/createInstantSearch'; import { createDisposeOptions, createInitOptions, createRenderOptions, } from '../../../../test/createWidget'; +import InstantSearch from '../../../instantsearch'; import connectRange from '../connectRange'; import type { AlgoliaSearchHelper } from 'algoliasearch-helper'; @@ -2190,7 +2190,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input it('passes the correct range set by initialUiState', () => { const searchClient = createSearchClient(); - const search = instantsearch({ + const search = new InstantSearch({ indexName: 'test-index', searchClient, initialUiState: { diff --git a/packages/instantsearch.js/src/connectors/range/connectRange.ts b/packages/instantsearch-core/src/connectors/range/connectRange.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/range/connectRange.ts rename to packages/instantsearch-core/src/connectors/range/connectRange.ts index b951b05e33c..04c6aa13f25 100644 --- a/packages/instantsearch.js/src/connectors/range/connectRange.ts +++ b/packages/instantsearch-core/src/connectors/range/connectRange.ts @@ -6,8 +6,9 @@ import { noop, } from '../../lib/utils'; +import type InstantSearch from '../../instantsearch'; import type { SendEventForFacet } from '../../lib/utils'; -import type { Connector, InstantSearch, WidgetRenderState } from '../../types'; +import type { Connector, WidgetRenderState } from '../../types'; import type { AlgoliaSearchHelper, SearchResults } from 'algoliasearch-helper'; const withUsage = createDocumentationMessageGenerator( diff --git a/packages/instantsearch.js/src/connectors/rating-menu/__tests__/connectRatingMenu-test.ts b/packages/instantsearch-core/src/connectors/rating-menu/__tests__/connectRatingMenu-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/rating-menu/__tests__/connectRatingMenu-test.ts rename to packages/instantsearch-core/src/connectors/rating-menu/__tests__/connectRatingMenu-test.ts diff --git a/packages/instantsearch.js/src/connectors/rating-menu/connectRatingMenu.ts b/packages/instantsearch-core/src/connectors/rating-menu/connectRatingMenu.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/rating-menu/connectRatingMenu.ts rename to packages/instantsearch-core/src/connectors/rating-menu/connectRatingMenu.ts diff --git a/packages/instantsearch.js/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts b/packages/instantsearch-core/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts rename to packages/instantsearch-core/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts diff --git a/packages/instantsearch.js/src/connectors/refinement-list/connectRefinementList.ts b/packages/instantsearch-core/src/connectors/refinement-list/connectRefinementList.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/refinement-list/connectRefinementList.ts rename to packages/instantsearch-core/src/connectors/refinement-list/connectRefinementList.ts diff --git a/packages/instantsearch.js/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts b/packages/instantsearch-core/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts rename to packages/instantsearch-core/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts diff --git a/packages/instantsearch.js/src/connectors/related-products/connectRelatedProducts.ts b/packages/instantsearch-core/src/connectors/related-products/connectRelatedProducts.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/related-products/connectRelatedProducts.ts rename to packages/instantsearch-core/src/connectors/related-products/connectRelatedProducts.ts diff --git a/packages/instantsearch.js/src/connectors/relevant-sort/__tests__/connectRelevantSort-test.ts b/packages/instantsearch-core/src/connectors/relevant-sort/__tests__/connectRelevantSort-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/relevant-sort/__tests__/connectRelevantSort-test.ts rename to packages/instantsearch-core/src/connectors/relevant-sort/__tests__/connectRelevantSort-test.ts diff --git a/packages/instantsearch.js/src/connectors/relevant-sort/connectRelevantSort.ts b/packages/instantsearch-core/src/connectors/relevant-sort/connectRelevantSort.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/relevant-sort/connectRelevantSort.ts rename to packages/instantsearch-core/src/connectors/relevant-sort/connectRelevantSort.ts diff --git a/packages/instantsearch.js/src/connectors/search-box/__tests__/connectSearchBox-test.ts b/packages/instantsearch-core/src/connectors/search-box/__tests__/connectSearchBox-test.ts similarity index 99% rename from packages/instantsearch.js/src/connectors/search-box/__tests__/connectSearchBox-test.ts rename to packages/instantsearch-core/src/connectors/search-box/__tests__/connectSearchBox-test.ts index 36e0dc506eb..50d330b1c31 100644 --- a/packages/instantsearch.js/src/connectors/search-box/__tests__/connectSearchBox-test.ts +++ b/packages/instantsearch-core/src/connectors/search-box/__tests__/connectSearchBox-test.ts @@ -17,7 +17,7 @@ import { createInitOptions, createRenderOptions, } from '../../../../test/createWidget'; -import InstantSearch from '../../../lib/InstantSearch'; +import InstantSearch from '../../../instantsearch'; import connectSearchBox from '../connectSearchBox'; describe('connectSearchBox', () => { diff --git a/packages/instantsearch.js/src/connectors/search-box/connectSearchBox.ts b/packages/instantsearch-core/src/connectors/search-box/connectSearchBox.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/search-box/connectSearchBox.ts rename to packages/instantsearch-core/src/connectors/search-box/connectSearchBox.ts diff --git a/packages/instantsearch.js/src/connectors/sort-by/__tests__/connectSortBy-test.ts b/packages/instantsearch-core/src/connectors/sort-by/__tests__/connectSortBy-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/sort-by/__tests__/connectSortBy-test.ts rename to packages/instantsearch-core/src/connectors/sort-by/__tests__/connectSortBy-test.ts diff --git a/packages/instantsearch.js/src/connectors/sort-by/connectSortBy.ts b/packages/instantsearch-core/src/connectors/sort-by/connectSortBy.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/sort-by/connectSortBy.ts rename to packages/instantsearch-core/src/connectors/sort-by/connectSortBy.ts diff --git a/packages/instantsearch.js/src/connectors/stats/__tests__/connectStats-test.ts b/packages/instantsearch-core/src/connectors/stats/__tests__/connectStats-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/stats/__tests__/connectStats-test.ts rename to packages/instantsearch-core/src/connectors/stats/__tests__/connectStats-test.ts diff --git a/packages/instantsearch.js/src/connectors/stats/connectStats.ts b/packages/instantsearch-core/src/connectors/stats/connectStats.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/stats/connectStats.ts rename to packages/instantsearch-core/src/connectors/stats/connectStats.ts diff --git a/packages/instantsearch.js/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.ts b/packages/instantsearch-core/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.ts rename to packages/instantsearch-core/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.ts diff --git a/packages/instantsearch.js/src/connectors/toggle-refinement/connectToggleRefinement.ts b/packages/instantsearch-core/src/connectors/toggle-refinement/connectToggleRefinement.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/toggle-refinement/connectToggleRefinement.ts rename to packages/instantsearch-core/src/connectors/toggle-refinement/connectToggleRefinement.ts diff --git a/packages/instantsearch-core/src/connectors/toggle-refinement/types.ts b/packages/instantsearch-core/src/connectors/toggle-refinement/types.ts new file mode 100644 index 00000000000..5ebbd8461bd --- /dev/null +++ b/packages/instantsearch-core/src/connectors/toggle-refinement/types.ts @@ -0,0 +1,6 @@ +export type { + /** @deprecated import from connectToggleRefinement directly */ + ToggleRefinementConnector, + /** @deprecated import from connectToggleRefinement directly */ + ToggleRefinementWidgetDescription, +} from './connectToggleRefinement'; diff --git a/packages/instantsearch.js/src/connectors/trending-facets/__tests__/connectTrendingFacets-test.ts b/packages/instantsearch-core/src/connectors/trending-facets/__tests__/connectTrendingFacets-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/trending-facets/__tests__/connectTrendingFacets-test.ts rename to packages/instantsearch-core/src/connectors/trending-facets/__tests__/connectTrendingFacets-test.ts diff --git a/packages/instantsearch.js/src/connectors/trending-facets/connectTrendingFacets.ts b/packages/instantsearch-core/src/connectors/trending-facets/connectTrendingFacets.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/trending-facets/connectTrendingFacets.ts rename to packages/instantsearch-core/src/connectors/trending-facets/connectTrendingFacets.ts diff --git a/packages/instantsearch.js/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts b/packages/instantsearch-core/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts rename to packages/instantsearch-core/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts diff --git a/packages/instantsearch.js/src/connectors/trending-items/connectTrendingItems.ts b/packages/instantsearch-core/src/connectors/trending-items/connectTrendingItems.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/trending-items/connectTrendingItems.ts rename to packages/instantsearch-core/src/connectors/trending-items/connectTrendingItems.ts diff --git a/packages/instantsearch.js/src/connectors/voice-search/__tests__/connectVoiceSearch-test.ts b/packages/instantsearch-core/src/connectors/voice-search/__tests__/connectVoiceSearch-test.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/voice-search/__tests__/connectVoiceSearch-test.ts rename to packages/instantsearch-core/src/connectors/voice-search/__tests__/connectVoiceSearch-test.ts diff --git a/packages/instantsearch.js/src/connectors/voice-search/connectVoiceSearch.ts b/packages/instantsearch-core/src/connectors/voice-search/connectVoiceSearch.ts similarity index 100% rename from packages/instantsearch.js/src/connectors/voice-search/connectVoiceSearch.ts rename to packages/instantsearch-core/src/connectors/voice-search/connectVoiceSearch.ts diff --git a/packages/instantsearch-core/src/helpers/get-insights-anonymous-user-token.ts b/packages/instantsearch-core/src/helpers/get-insights-anonymous-user-token.ts new file mode 100644 index 00000000000..9e698a0e519 --- /dev/null +++ b/packages/instantsearch-core/src/helpers/get-insights-anonymous-user-token.ts @@ -0,0 +1,39 @@ +import { warning } from '../lib/utils'; + +export const ANONYMOUS_TOKEN_COOKIE_KEY = '_ALGOLIA'; + +function getCookie(name: string): string | undefined { + if (typeof document !== 'object' || typeof document.cookie !== 'string') { + return undefined; + } + + const prefix = `${name}=`; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i]; + while (cookie.charAt(0) === ' ') { + cookie = cookie.substring(1); + } + if (cookie.indexOf(prefix) === 0) { + return cookie.substring(prefix.length, cookie.length); + } + } + return undefined; +} + +export function getInsightsAnonymousUserTokenInternal(): string | undefined { + return getCookie(ANONYMOUS_TOKEN_COOKIE_KEY); +} + +/** + * @deprecated This function will be still supported in 4.x releases, but not further. It is replaced by the `insights` middleware. For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/ + */ +export default function getInsightsAnonymousUserToken(): string | undefined { + warning( + false, + `\`getInsightsAnonymousUserToken\` function has been deprecated. It is still supported in 4.x releases, but not further. It is replaced by the \`insights\` middleware. + +For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/` + ); + return getInsightsAnonymousUserTokenInternal(); +} diff --git a/packages/instantsearch-core/src/helpers/index.ts b/packages/instantsearch-core/src/helpers/index.ts new file mode 100644 index 00000000000..f5905c4010b --- /dev/null +++ b/packages/instantsearch-core/src/helpers/index.ts @@ -0,0 +1 @@ +export { getInsightsAnonymousUserTokenInternal } from './get-insights-anonymous-user-token'; diff --git a/packages/instantsearch-core/src/index.ts b/packages/instantsearch-core/src/index.ts new file mode 100644 index 00000000000..46ae7a39afc --- /dev/null +++ b/packages/instantsearch-core/src/index.ts @@ -0,0 +1,79 @@ +export { default as version } from './version'; +export { default as InstantSearch } from './instantsearch'; +export { instantsearch } from './instantsearch'; +export { INSTANTSEARCH_FUTURE_DEFAULTS } from './instantsearch'; +export type { + InstantSearchOptions, + InstantSearchStatus, +} from './instantsearch'; +export * from './connectors'; +export { AbstractChat } from './lib/ai-lite'; +export { parseJsonEventStream, processStream } from './lib/ai-lite'; +export type { + ChatStatus, + UIDataTypes, + UITool, + UITools, + ProviderMetadata, + TextUIPart, + ReasoningUIPart, + SourceUrlUIPart, + SourceDocumentUIPart, + FileUIPart, + StepStartUIPart, + DataUIPart, + ToolUIPart, + DynamicToolUIPart, + UIMessagePart, + UIMessage, + InferUIMessageMetadata, + InferUIMessageData, + InferUIMessageTools, + InferUIMessageToolCall, + InferUIMessageChunk, + UIMessageChunk, + ChatTransport, + ChatRequestOptions, + ChatInit, + IdGenerator, + ChatOnErrorCallback, + ChatOnToolCallCallback, + ChatOnFinishCallback, + ChatOnDataCallback, + CreateUIMessage, +} from './lib/ai-lite'; +export { + Chat, + ChatState, + CACHE_KEY, + SearchIndexToolType, + RecommendToolType, + MemorizeToolType, + MemorySearchToolType, + PonderToolType, + DisplayResultsToolType, + openChat, + isChatBusy, +} from './lib/chat'; +export type { OpenChatOptions } from './lib/chat'; +export { default as createVoiceSearchHelper } from './lib/voiceSearchHelper'; +export type { + CreateVoiceSearchHelper, + VoiceSearchHelper, + VoiceSearchHelperParams, + VoiceListeningState, +} from './lib/voiceSearchHelper/types'; +export * from './lib/infiniteHitsCache'; +export * from './lib/utils'; +export * from './lib/routers'; +export * from './lib/server'; +export * from './lib/stateMappings'; +export * from './middlewares'; +export * from './types'; +export * from './widgets'; +export type { + FacetRefinement, + NumericRefinement, + Refinement, +} from './lib/utils'; +export type { InsightsEvent } from './middlewares'; diff --git a/packages/instantsearch-core/src/instantsearch.ts b/packages/instantsearch-core/src/instantsearch.ts new file mode 100644 index 00000000000..a45364920b0 --- /dev/null +++ b/packages/instantsearch-core/src/instantsearch.ts @@ -0,0 +1,923 @@ +import EventEmitter from '@algolia/events'; +import algoliasearchHelper from 'algoliasearch-helper'; + +import { + createDocumentationMessageGenerator, + createDocumentationLink, + defer, + hydrateRecommendCache, + hydrateSearchClient, + noop, + warning, + setIndexHelperState, + isIndexWidget, +} from './lib/utils'; +import { + createInsightsMiddleware, + createMetadataMiddleware, + createRouterMiddleware, + isMetadataEnabled, +} from './middlewares'; +import version from './version'; +import { index as indexWidget } from './widgets'; + +import type { + InsightsEvent, + InsightsProps, + RouterProps, +} from './middlewares'; +import type { + InsightsClient as AlgoliaInsightsClient, + SearchClient, + Widget, + IndexWidget, + UiState, + CreateURL, + Middleware, + MiddlewareDefinition, + RenderState, + InitialResults, + CompositionClient, +} from './types'; +import type { Expand } from './types/utils'; +import type { AlgoliaSearchHelper } from 'algoliasearch-helper'; + +const withUsage = createDocumentationMessageGenerator({ + name: 'instantsearch', +}); + +function defaultCreateURL() { + return '#'; +} + +// this purposely breaks typescript's type inference to ensure it's not used +// as it's used for a default parameter for example +// source: https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-504042546 +type NoInfer = T extends infer S ? S : never; + +/** + * Global options for an InstantSearch instance. + */ +export type InstantSearchOptions< + TUiState extends UiState = UiState, + TRouteState = TUiState +> = { + /** + * The name of the main index. If no indexName is provided, you have to manually add an index widget. + */ + indexName?: string; + + /** + * The objectID of the composition. + * If this is passed, the composition API will be used for search. + * Multi-index search is not supported with this option. + */ + compositionID?: string; + + /** + * The search client to plug to InstantSearch.js + * + * Usage: + * ```javascript + * // Using the default Algolia search client + * instantsearch({ + * indexName: 'indexName', + * searchClient: algoliasearch('appId', 'apiKey') + * }); + * + * // Using a custom search client + * instantsearch({ + * indexName: 'indexName', + * searchClient: { + * search(requests) { + * // fetch response based on requests + * return response; + * }, + * searchForFacetValues(requests) { + * // fetch response based on requests + * return response; + * } + * } + * }); + * ``` + */ + searchClient: SearchClient | CompositionClient; + + /** + * The locale used to display numbers. This will be passed + * to `Number.prototype.toLocaleString()` + */ + numberLocale?: string; + + /** + * A hook that will be called each time a search needs to be done, with the + * helper as a parameter. It's your responsibility to call `helper.search()`. + * This option allows you to avoid doing searches at page load for example. + * @deprecated use onStateChange instead + */ + searchFunction?: (helper: AlgoliaSearchHelper) => void; + + /** + * Function called when the state changes. + * + * Using this function makes the instance controlled. This means that you + * become in charge of updating the UI state with the `setUiState` function. + */ + onStateChange?: (params: { + uiState: TUiState; + setUiState: ( + uiState: TUiState | ((previousUiState: TUiState) => TUiState) + ) => void; + }) => void; + + /** + * Injects a `uiState` to the `instantsearch` instance. You can use this option + * to provide an initial state to a widget. Note that the state is only used + * for the first search. To unconditionally pass additional parameters to the + * Algolia API, take a look at the `configure` widget. + */ + initialUiState?: NoInfer; + + /** + * Time before a search is considered stalled. The default is 200ms + */ + stalledSearchDelay?: number; + + /** + * Router configuration used to save the UI State into the URL or any other + * client side persistence. Passing `true` will use the default URL options. + */ + routing?: RouterProps | boolean; + + /** + * Enables the Insights middleware and loads the Insights library + * if not already loaded. + * + * The Insights middleware sends view and click events automatically, and lets + * you set up your own events. + * + * @default false + */ + insights?: InsightsProps | boolean; + + /** + * the instance of search-insights to use for sending insights events inside + * widgets like `hits`. + * + * @deprecated This property will be still supported in 4.x releases, but not further. It is replaced by the `insights` middleware. For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/ + */ + insightsClient?: AlgoliaInsightsClient; + future?: { + /** + * Changes the way `dispose` is used in InstantSearch lifecycle. + * + * If `false` (by default), each widget unmounting will remove its state as well, even if there are multiple widgets reading that UI State. + * + * If `true`, each widget unmounting will only remove its own state if it's the last of its type. This allows for dynamically adding and removing widgets without losing their state. + * + * @default false + */ + // @MAJOR: Remove legacy behaviour + preserveSharedStateOnUnmount?: boolean; + /** + * Changes the way root levels of hierarchical facets have their count displayed. + * + * If `false` (by default), the count of the refined root level is updated to match the count of the actively refined parent level. + * + * If `true`, the count of the root level stays the same as the count of all children levels. + * + * @default false + */ + // @MAJOR: Remove legacy behaviour here and in algoliasearch-helper + persistHierarchicalRootCount?: boolean; + }; + + /** + * @internal Injected by the InstantSearch.js factory to supply Hogan + * template helpers (`highlight`, `snippet`, `formatNumber`, …). Not part of + * the public API. + */ + _internalHelpers?: Record; +}; + +export type InstantSearchStatus = 'idle' | 'loading' | 'stalled' | 'error'; + +export const INSTANTSEARCH_FUTURE_DEFAULTS: Required< + InstantSearchOptions['future'] +> = { + preserveSharedStateOnUnmount: false, + persistHierarchicalRootCount: false, +}; + +/** + * The actual implementation of the InstantSearch. This is + * created using the `instantsearch` factory function. + * It emits the 'render' event every time a search is done + */ +class InstantSearch< + TUiState extends UiState = UiState, + TRouteState = TUiState +> extends EventEmitter { + public client: InstantSearchOptions['searchClient']; + public indexName: string; + public compositionID?: string; + public insightsClient: AlgoliaInsightsClient | null; + public onStateChange: InstantSearchOptions['onStateChange'] | null = + null; + public future: NonNullable['future']>; + public helper: AlgoliaSearchHelper | null; + public mainHelper: AlgoliaSearchHelper | null; + public mainIndex: IndexWidget; + public started: boolean; + public templatesConfig: Record; + public renderState: RenderState = {}; + public _stalledSearchDelay: number; + public _searchStalledTimer: any; + public _initialUiState: TUiState; + public _initialResults: InitialResults | null; + public _manuallyResetScheduleSearch: boolean = false; + public _resetScheduleSearch?: () => void; + public _createURL: CreateURL; + public _searchFunction?: InstantSearchOptions['searchFunction']; + public _mainHelperSearch?: AlgoliaSearchHelper['search']; + public _hasSearchWidget: boolean = false; + public _hasRecommendWidget: boolean = false; + public _insights: InstantSearchOptions['insights']; + public middleware: Array<{ + creator: Middleware; + instance: MiddlewareDefinition; + }> = []; + public sendEventToInsights: (event: InsightsEvent) => void; + /** + * The status of the search. Can be "idle", "loading", "stalled", or "error". + */ + public status: InstantSearchStatus = 'idle'; + /** + * The last returned error from the Search API. + * The error gets cleared when the next valid search response is rendered. + */ + public error: Error | undefined = undefined; + + /** + * @deprecated use `status === 'stalled'` instead + */ + public get _isSearchStalled(): boolean { + warning( + false, + `\`InstantSearch._isSearchStalled\` is deprecated and will be removed in InstantSearch.js 5.0. + +Use \`InstantSearch.status === "stalled"\` instead.` + ); + + return this.status === 'stalled'; + } + + public constructor(options: InstantSearchOptions) { + super(); + + // prevent `render` event listening from causing a warning + this.setMaxListeners(100); + + const { + indexName = '', + compositionID, + initialUiState = {} as TUiState, + routing = null, + insights = undefined, + searchFunction, + stalledSearchDelay = 200, + searchClient = null, + insightsClient = null, + onStateChange = null, + future = { + ...INSTANTSEARCH_FUTURE_DEFAULTS, + ...(options.future || {}), + }, + } = options; + + if (searchClient === null) { + throw new Error(withUsage('The `searchClient` option is required.')); + } + + if (typeof searchClient.search !== 'function') { + throw new Error( + `The \`searchClient\` must implement a \`search\` method. + +See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/` + ); + } + + if ( + typeof (searchClient as { addAlgoliaAgent?: unknown }) + .addAlgoliaAgent === 'function' + ) { + ( + searchClient as { addAlgoliaAgent: (agent: string) => void } + ).addAlgoliaAgent(`instantsearch-core (${version})`); + } + + warning( + insightsClient === null, + `\`insightsClient\` property has been deprecated. It is still supported in 4.x releases, but not further. It is replaced by the \`insights\` middleware. + +For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/` + ); + + if (insightsClient && typeof insightsClient !== 'function') { + throw new Error( + withUsage('The `insightsClient` option should be a function.') + ); + } + + warning( + !(options as any).searchParameters, + `The \`searchParameters\` option is deprecated and will not be supported in InstantSearch.js 4.x. + +You can replace it with the \`configure\` widget: + +\`\`\` +search.addWidgets([ + configure(${JSON.stringify((options as any).searchParameters, null, 2)}) +]); +\`\`\` + +See ${createDocumentationLink({ + name: 'configure', + })}` + ); + + if (__DEV__ && options.future?.preserveSharedStateOnUnmount === undefined) { + // eslint-disable-next-line no-console + console.info(`Starting from the next major version, InstantSearch will change how widgets state is preserved when they are removed. InstantSearch will keep the state of unmounted widgets to be usable by other widgets with the same attribute. + +We recommend setting \`future.preserveSharedStateOnUnmount\` to true to adopt this change today. +To stay with the current behaviour and remove this warning, set the option to false. + +See documentation: ${createDocumentationLink({ + name: 'instantsearch', + })}#widget-param-future + `); + } + + this.client = searchClient; + this.future = future; + this.insightsClient = insightsClient; + this.indexName = indexName; + this.compositionID = compositionID; + this.helper = null; + this.mainHelper = null; + this.mainIndex = indexWidget({ + // we use an index widget to render compositions + // this only works because there's only one composition index allow for now + indexName: this.compositionID || this.indexName, + }); + this.onStateChange = onStateChange; + + this.started = false; + this.templatesConfig = { + helpers: options._internalHelpers ?? {}, + compileOptions: {}, + }; + + this._stalledSearchDelay = stalledSearchDelay; + this._searchStalledTimer = null; + + this._createURL = defaultCreateURL; + this._initialUiState = initialUiState as TUiState; + this._initialResults = null; + + this._insights = insights; + + if (searchFunction) { + warning( + false, + `The \`searchFunction\` option is deprecated. Use \`onStateChange\` instead.` + ); + this._searchFunction = searchFunction; + } + + this.sendEventToInsights = noop; + + if (routing) { + const routerOptions = typeof routing === 'boolean' ? {} : routing; + routerOptions.$$internal = true; + this.use(createRouterMiddleware(routerOptions)); + } + + // This is the default Insights middleware, + // added when `insights` is set to true by the user. + // Any user-provided middleware will be added later and override this one. + if (insights) { + const insightsOptions = typeof insights === 'boolean' ? {} : insights; + insightsOptions.$$internal = true; + this.use(createInsightsMiddleware(insightsOptions)); + } + + if (isMetadataEnabled()) { + this.use(createMetadataMiddleware({ $$internal: true })); + } + } + + /** + * Hooks a middleware into the InstantSearch lifecycle. + */ + public use(...middleware: Array>): this { + const newMiddlewareList = middleware.map((fn) => { + const newMiddleware = { + $$type: '__unknown__', + $$internal: false, + subscribe: noop, + started: noop, + unsubscribe: noop, + onStateChange: noop, + ...fn({ + instantSearchInstance: this as unknown as InstantSearch< + UiState, + UiState + >, + }), + }; + this.middleware.push({ + creator: fn, + instance: newMiddleware, + }); + return newMiddleware; + }); + + // If the instance has already started, we directly subscribe the + // middleware so they're notified of changes. + if (this.started) { + newMiddlewareList.forEach((m) => { + m.subscribe(); + m.started(); + }); + } + + return this; + } + + /** + * Removes a middleware from the InstantSearch lifecycle. + */ + public unuse(...middlewareToUnuse: Array>): this { + this.middleware + .filter((m) => middlewareToUnuse.includes(m.creator)) + .forEach((m) => m.instance.unsubscribe()); + + this.middleware = this.middleware.filter( + (m) => !middlewareToUnuse.includes(m.creator) + ); + + return this; + } + + // @major we shipped with EXPERIMENTAL_use, but have changed that to just `use` now + public EXPERIMENTAL_use(...middleware: Middleware[]): this { + warning( + false, + 'The middleware API is now considered stable, so we recommend replacing `EXPERIMENTAL_use` with `use` before upgrading to the next major version.' + ); + + return this.use(...middleware); + } + + /** + * Adds a widget to the search instance. + * A widget can be added either before or after InstantSearch has started. + * @param widget The widget to add to InstantSearch. + * + * @deprecated This method will still be supported in 4.x releases, but not further. It is replaced by `addWidgets([widget])`. + */ + public addWidget(widget: Widget) { + warning( + false, + 'addWidget will still be supported in 4.x releases, but not further. It is replaced by `addWidgets([widget])`' + ); + + return this.addWidgets([widget]); + } + + /** + * Adds multiple widgets to the search instance. + * Widgets can be added either before or after InstantSearch has started. + * @param widgets The array of widgets to add to InstantSearch. + */ + public addWidgets( + widgets: Array> + ) { + if (!Array.isArray(widgets)) { + throw new Error( + withUsage( + 'The `addWidgets` method expects an array of widgets. Please use `addWidget`.' + ) + ); + } + + if ( + this.compositionID && + widgets.some((w) => !Array.isArray(w) && isIndexWidget(w) && !w._isolated) + ) { + throw new Error( + withUsage( + 'The `index` widget cannot be used with a composition-based InstantSearch implementation.' + ) + ); + } + + this.mainIndex.addWidgets(widgets); + + return this; + } + + /** + * Removes a widget from the search instance. + * @deprecated This method will still be supported in 4.x releases, but not further. It is replaced by `removeWidgets([widget])` + * @param widget The widget instance to remove from InstantSearch. + * + * The widget must implement a `dispose()` method to clear its state. + */ + public removeWidget(widget: Widget | IndexWidget) { + warning( + false, + 'removeWidget will still be supported in 4.x releases, but not further. It is replaced by `removeWidgets([widget])`' + ); + + return this.removeWidgets([widget]); + } + + /** + * Removes multiple widgets from the search instance. + * @param widgets Array of widgets instances to remove from InstantSearch. + * + * The widgets must implement a `dispose()` method to clear their states. + */ + public removeWidgets(widgets: Array) { + if (!Array.isArray(widgets)) { + throw new Error( + withUsage( + 'The `removeWidgets` method expects an array of widgets. Please use `removeWidget`.' + ) + ); + } + + this.mainIndex.removeWidgets(widgets); + + return this; + } + + /** + * Ends the initialization of InstantSearch.js and triggers the + * first search. + */ + public start() { + if (this.started) { + throw new Error( + withUsage('The `start` method has already been called once.') + ); + } + + // This Helper is used for the queries, we don't care about its state. The + // states are managed at the `index` level. We use this Helper to create + // DerivedHelper scoped into the `index` widgets. + // In Vue InstantSearch' hydrate, a main helper gets set before start, so + // we need to respect this helper as a way to keep all listeners correct. + const mainHelper = + this.mainHelper || + algoliasearchHelper(this.client, this.indexName, undefined, { + persistHierarchicalRootCount: this.future.persistHierarchicalRootCount, + }); + + if (this.compositionID) { + mainHelper.searchForFacetValues = + mainHelper.searchForCompositionFacetValues.bind(mainHelper); + } + + mainHelper.search = () => { + this.status = 'loading'; + this.scheduleRender(false); + + warning( + Boolean(this.indexName) || + Boolean(this.compositionID) || + this.mainIndex.getWidgets().some(isIndexWidget), + 'No indexName provided, nor an explicit index widget in the widgets tree. This is required to be able to display results.' + ); + + // This solution allows us to keep the exact same API for the users but + // under the hood, we have a different implementation. It should be + // completely transparent for the rest of the codebase. Only this module + // is impacted. + if (this._hasSearchWidget) { + if (this.compositionID) { + mainHelper.searchWithComposition(); + } else { + mainHelper.searchOnlyWithDerivedHelpers(); + } + } + + if (this._hasRecommendWidget) { + mainHelper.recommend(); + } + + return mainHelper; + }; + + if (this._searchFunction) { + // this client isn't used to actually search, but required for the helper + // to not throw errors + const fakeClient = { + search: () => new Promise(noop), + } as any as SearchClient; + + this._mainHelperSearch = mainHelper.search.bind(mainHelper); + mainHelper.search = () => { + const mainIndexHelper = this.mainIndex.getHelper(); + const searchFunctionHelper = algoliasearchHelper( + fakeClient, + mainIndexHelper!.state.index, + mainIndexHelper!.state + ); + searchFunctionHelper.once('search', ({ state }) => { + mainIndexHelper!.overrideStateWithoutTriggeringChangeEvent(state); + this._mainHelperSearch!(); + }); + // Forward state changes from `searchFunctionHelper` to `mainIndexHelper` + searchFunctionHelper.on('change', ({ state }) => { + mainIndexHelper!.setState(state); + }); + this._searchFunction!(searchFunctionHelper); + return mainHelper; + }; + } + + // Only the "main" Helper emits the `error` event vs the one for `search` + // and `results` that are also emitted on the derived one. + mainHelper.on('error', ({ error }) => { + if (!(error instanceof Error)) { + // typescript lies here, error is in some cases { name: string, message: string } + const err = error as Record; + error = Object.keys(err).reduce((acc, key) => { + (acc as any)[key] = err[key]; + return acc; + }, new Error(err.message)); + } + // If an error is emitted, it is re-thrown by events. In previous versions + // we emitted {error}, which is thrown as: + // "Uncaught, unspecified \"error\" event. ([object Object])" + // To avoid breaking changes, we make the error available in both + // `error` and `error.error` + // @MAJOR emit only error + (error as any).error = error; + this.error = error; + this.status = 'error'; + this.scheduleRender(false); + + // This needs to execute last because it throws the error. + this.emit('error', error); + }); + + this.mainHelper = mainHelper; + + this.middleware.forEach(({ instance }) => { + instance.subscribe(); + }); + + this.mainIndex.init({ + instantSearchInstance: this as unknown as InstantSearch, + parent: null, + uiState: this._initialUiState, + }); + + if (this._initialResults) { + hydrateSearchClient(this.client, this._initialResults); + hydrateRecommendCache(this.mainHelper, this._initialResults); + + const originalScheduleSearch = this.scheduleSearch; + // We don't schedule a first search when initial results are provided + // because we already have the results to render. This skips the initial + // network request on the browser on `start`. + this.scheduleSearch = defer(noop); + if (this._manuallyResetScheduleSearch) { + // If `_manuallyResetScheduleSearch` is passed, it means that we don't + // want to rely on a single `defer` to reset the `scheduleSearch`. + // Instead, the consumer will call `_resetScheduleSearch` to restore + // the original `scheduleSearch` function. + // This happens in the React flavour after rendering. + this._resetScheduleSearch = () => { + this.scheduleSearch = originalScheduleSearch; + }; + } else { + // We also skip the initial network request when widgets are dynamically + // added in the first tick (that's the case in all the framework-based flavors). + // When we add a widget to `index`, it calls `scheduleSearch`. We can rely + // on our `defer` util to restore the original `scheduleSearch` value once + // widgets are added to hook back to the regular lifecycle. + defer(() => { + this.scheduleSearch = originalScheduleSearch; + })(); + } + } + // We only schedule a search when widgets have been added before `start()` + // because there are listeners that can use these results. + // This is especially useful in framework-based flavors that wait for + // dynamically-added widgets to trigger a network request. It avoids + // having to batch this initial network request with the one coming from + // `addWidgets()`. + // Later, we could also skip `index()` widgets and widgets that don't read + // the results, but this is an optimization that has a very low impact for now. + else if (this.mainIndex.getWidgets().length > 0) { + this.scheduleSearch(); + } + + // Keep the previous reference for legacy purpose, some pattern use + // the direct Helper access `search.helper` (e.g multi-index). + this.helper = this.mainIndex.getHelper(); + + // track we started the search if we add more widgets, + // to init them directly after add + this.started = true; + + this.middleware.forEach(({ instance }) => { + instance.started(); + }); + + // This is the automatic Insights middleware, + // added when `insights` is unset and the initial results possess `queryID`. + // Any user-provided middleware will be added later and override this one. + if (typeof this._insights === 'undefined') { + mainHelper.derivedHelpers[0].once('result', () => { + const hasAutomaticInsights = this.mainIndex + .getScopedResults() + .some(({ results }) => results?._automaticInsights); + if (hasAutomaticInsights) { + this.use( + createInsightsMiddleware({ + $$internal: true, + $$automatic: true, + }) + ); + } + }); + } + } + + /** + * Removes all widgets without triggering a search afterwards. + * @return {undefined} This method does not return anything + */ + public dispose(): void { + this.scheduleSearch.cancel(); + this.scheduleRender.cancel(); + clearTimeout(this._searchStalledTimer); + + this.removeWidgets(this.mainIndex.getWidgets()); + this.mainIndex.dispose(); + + // You can not start an instance two times, therefore a disposed instance + // needs to set started as false otherwise this can not be restarted at a + // later point. + this.started = false; + + // The helper needs to be reset to perform the next search from a fresh state. + // If not reset, it would use the state stored before calling `dispose()`. + this.removeAllListeners(); + this.mainHelper?.removeAllListeners(); + this.mainHelper = null; + this.helper = null; + + this.middleware.forEach(({ instance }) => { + instance.unsubscribe(); + }); + } + + public scheduleSearch = defer(() => { + if (this.started) { + this.mainHelper!.search(); + } + }); + + public scheduleRender = defer((shouldResetStatus: boolean = true) => { + if (!this.mainHelper?.hasPendingRequests()) { + clearTimeout(this._searchStalledTimer); + this._searchStalledTimer = null; + + if (shouldResetStatus) { + this.status = 'idle'; + this.error = undefined; + } + } + + this.mainIndex.render({ + instantSearchInstance: this as unknown as InstantSearch, + }); + + this.emit('render'); + }); + + public scheduleStalledRender() { + if (!this._searchStalledTimer) { + this._searchStalledTimer = setTimeout(() => { + this.status = 'stalled'; + this.scheduleRender(); + }, this._stalledSearchDelay); + } + } + + /** + * Set the UI state and trigger a search. + * @param uiState The next UI state or a function computing it from the current state + * @param callOnStateChange private parameter used to know if the method is called from a state change + */ + public setUiState( + uiState: TUiState | ((previousUiState: TUiState) => TUiState), + callOnStateChange: boolean = true + ): void { + if (!this.mainHelper) { + throw new Error( + withUsage('The `start` method needs to be called before `setUiState`.') + ); + } + + // We refresh the index UI state to update the local UI state that the + // main index passes to the function form of `setUiState`. + this.mainIndex.refreshUiState(); + const nextUiState = + typeof uiState === 'function' + ? uiState(this.mainIndex.getWidgetUiState({}) as TUiState) + : uiState; + + if (this.onStateChange && callOnStateChange) { + this.onStateChange({ + uiState: nextUiState, + setUiState: (finalUiState) => { + setIndexHelperState( + typeof finalUiState === 'function' + ? finalUiState(nextUiState) + : finalUiState, + this.mainIndex + ); + + this.scheduleSearch(); + this.onInternalStateChange(); + }, + }); + } else { + setIndexHelperState(nextUiState, this.mainIndex); + + this.scheduleSearch(); + this.onInternalStateChange(); + } + } + + public getUiState(): TUiState { + if (this.started) { + // We refresh the index UI state to make sure changes from `refine` are taken in account + this.mainIndex.refreshUiState(); + } + + return this.mainIndex.getWidgetUiState({}) as TUiState; + } + + public onInternalStateChange = defer(() => { + const nextUiState = this.mainIndex.getWidgetUiState({}) as TUiState; + + this.middleware.forEach(({ instance }) => { + instance.onStateChange({ + uiState: nextUiState, + }); + }); + }); + + public createURL(nextState: TUiState = {} as TUiState): string { + if (!this.started) { + throw new Error( + withUsage('The `start` method needs to be called before `createURL`.') + ); + } + + return this._createURL(nextState); + } + + public refresh() { + if (!this.mainHelper) { + throw new Error( + withUsage('The `start` method needs to be called before `refresh`.') + ); + } + + this.mainHelper.clearCache().search(); + } +} + +export default InstantSearch; + +type InstantSearchModule = { + , TRouteState = TUiState>( + options: InstantSearchOptions, TRouteState> + ): InstantSearch, TRouteState>; + version: string; +}; + +export const instantsearch: InstantSearchModule = (options) => + new InstantSearch(options); +instantsearch.version = version; diff --git a/packages/instantsearch.js/src/lib/ai-lite/__tests__/abstract-chat.test.ts b/packages/instantsearch-core/src/lib/ai-lite/__tests__/abstract-chat.test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/__tests__/abstract-chat.test.ts rename to packages/instantsearch-core/src/lib/ai-lite/__tests__/abstract-chat.test.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/__tests__/stream-parser.test.ts b/packages/instantsearch-core/src/lib/ai-lite/__tests__/stream-parser-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/__tests__/stream-parser.test.ts rename to packages/instantsearch-core/src/lib/ai-lite/__tests__/stream-parser-test.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/__tests__/utils.test.ts b/packages/instantsearch-core/src/lib/ai-lite/__tests__/utils.test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/__tests__/utils.test.ts rename to packages/instantsearch-core/src/lib/ai-lite/__tests__/utils.test.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/abstract-chat.ts b/packages/instantsearch-core/src/lib/ai-lite/abstract-chat.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/abstract-chat.ts rename to packages/instantsearch-core/src/lib/ai-lite/abstract-chat.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/index.ts b/packages/instantsearch-core/src/lib/ai-lite/index.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/index.ts rename to packages/instantsearch-core/src/lib/ai-lite/index.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/stream-parser.ts b/packages/instantsearch-core/src/lib/ai-lite/stream-parser.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/stream-parser.ts rename to packages/instantsearch-core/src/lib/ai-lite/stream-parser.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/transport.ts b/packages/instantsearch-core/src/lib/ai-lite/transport.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/transport.ts rename to packages/instantsearch-core/src/lib/ai-lite/transport.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/types.ts b/packages/instantsearch-core/src/lib/ai-lite/types.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/types.ts rename to packages/instantsearch-core/src/lib/ai-lite/types.ts diff --git a/packages/instantsearch.js/src/lib/ai-lite/utils.ts b/packages/instantsearch-core/src/lib/ai-lite/utils.ts similarity index 100% rename from packages/instantsearch.js/src/lib/ai-lite/utils.ts rename to packages/instantsearch-core/src/lib/ai-lite/utils.ts diff --git a/packages/instantsearch.js/src/lib/chat/__tests__/chat.test.ts b/packages/instantsearch-core/src/lib/chat/__tests__/chat-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/chat/__tests__/chat.test.ts rename to packages/instantsearch-core/src/lib/chat/__tests__/chat-test.ts diff --git a/packages/instantsearch.js/src/lib/chat/__tests__/openChat.test.ts b/packages/instantsearch-core/src/lib/chat/__tests__/openChat.test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/chat/__tests__/openChat.test.ts rename to packages/instantsearch-core/src/lib/chat/__tests__/openChat.test.ts diff --git a/packages/instantsearch.js/src/lib/chat/chat.ts b/packages/instantsearch-core/src/lib/chat/chat.ts similarity index 100% rename from packages/instantsearch.js/src/lib/chat/chat.ts rename to packages/instantsearch-core/src/lib/chat/chat.ts diff --git a/packages/instantsearch.js/src/lib/chat/index.ts b/packages/instantsearch-core/src/lib/chat/index.ts similarity index 94% rename from packages/instantsearch.js/src/lib/chat/index.ts rename to packages/instantsearch-core/src/lib/chat/index.ts index 9047b677b38..9b3554f9332 100644 --- a/packages/instantsearch.js/src/lib/chat/index.ts +++ b/packages/instantsearch-core/src/lib/chat/index.ts @@ -3,6 +3,7 @@ export type { ChatInit } from './chat'; export { AbstractChat } from './chat'; export { ChatState } from './chat'; export { Chat } from './chat'; +export { CACHE_KEY } from './chat'; export { openChat, isChatBusy } from './openChat'; export type { OpenChatOptions } from './openChat'; diff --git a/packages/instantsearch.js/src/lib/chat/openChat.ts b/packages/instantsearch-core/src/lib/chat/openChat.ts similarity index 100% rename from packages/instantsearch.js/src/lib/chat/openChat.ts rename to packages/instantsearch-core/src/lib/chat/openChat.ts diff --git a/packages/instantsearch-core/src/lib/infiniteHitsCache/index.ts b/packages/instantsearch-core/src/lib/infiniteHitsCache/index.ts new file mode 100644 index 00000000000..6fd47d4f820 --- /dev/null +++ b/packages/instantsearch-core/src/lib/infiniteHitsCache/index.ts @@ -0,0 +1 @@ +export { default as createInfiniteHitsSessionStorageCache } from './sessionStorage'; diff --git a/packages/instantsearch-core/src/lib/infiniteHitsCache/sessionStorage.ts b/packages/instantsearch-core/src/lib/infiniteHitsCache/sessionStorage.ts new file mode 100644 index 00000000000..af4d74d6fbd --- /dev/null +++ b/packages/instantsearch-core/src/lib/infiniteHitsCache/sessionStorage.ts @@ -0,0 +1,74 @@ +import { isEqual, safelyRunOnBrowser } from '../utils'; + +import type { InfiniteHitsCache } from '../../connectors/infinite-hits/connectInfiniteHits'; +import type { PlainSearchParameters } from 'algoliasearch-helper'; + +function getStateWithoutPage(state: PlainSearchParameters) { + const { page, ...rest } = state || {}; + return rest; +} + +export default function createInfiniteHitsSessionStorageCache({ + key, +}: { + /** + * If you display multiple instances of infiniteHits on the same page, + * you must provide a unique key for each instance. + */ + key?: string; +} = {}): InfiniteHitsCache { + const KEY = ['ais.infiniteHits', key].filter(Boolean).join(':'); + + return { + read({ state }) { + const sessionStorage = safelyRunOnBrowser( + ({ window }) => window.sessionStorage + ); + + if (!sessionStorage) { + return null; + } + + try { + const cache = JSON.parse( + // @ts-expect-error JSON.parse() requires a string, but it actually accepts null, too. + sessionStorage.getItem(KEY) + ); + + return cache && isEqual(cache.state, getStateWithoutPage(state)) + ? cache.hits + : null; + } catch (error) { + if (error instanceof SyntaxError) { + try { + sessionStorage.removeItem(KEY); + } catch (err) { + // do nothing + } + } + return null; + } + }, + write({ state, hits }) { + const sessionStorage = safelyRunOnBrowser( + ({ window }) => window.sessionStorage + ); + + if (!sessionStorage) { + return; + } + + try { + sessionStorage.setItem( + KEY, + JSON.stringify({ + state: getStateWithoutPage(state), + hits, + }) + ); + } catch (error) { + // do nothing + } + }, + }; +} diff --git a/packages/instantsearch-core/src/lib/insights/client.ts b/packages/instantsearch-core/src/lib/insights/client.ts new file mode 100644 index 00000000000..99bd1f73e1d --- /dev/null +++ b/packages/instantsearch-core/src/lib/insights/client.ts @@ -0,0 +1,135 @@ +import { + uniq, + find, + createDocumentationMessageGenerator, + warning, +} from '../utils'; + +import type { + Hit, + InsightsClient, + InsightsClientMethod, + InsightsClientPayload, + Connector, +} from '../../types'; +import type { SearchResults } from 'algoliasearch-helper'; + +const getSelectedHits = (hits: Hit[], selectedObjectIDs: string[]): Hit[] => { + return selectedObjectIDs.map((objectID) => { + const hit = find(hits, (h) => h.objectID === objectID); + if (typeof hit === 'undefined') { + throw new Error( + `Could not find objectID "${objectID}" passed to \`clickedObjectIDsAfterSearch\` in the returned hits. This is necessary to infer the absolute position and the query ID.` + ); + } + return hit; + }); +}; + +const getQueryID = (selectedHits: Hit[]): string => { + const queryIDs = uniq(selectedHits.map((hit) => hit.__queryID)); + if (queryIDs.length > 1) { + throw new Error( + 'Insights currently allows a single `queryID`. The `objectIDs` provided map to multiple `queryID`s.' + ); + } + const queryID = queryIDs[0]; + if (typeof queryID !== 'string') { + throw new Error( + `Could not infer \`queryID\`. Ensure InstantSearch \`clickAnalytics: true\` was added with the Configure widget. + +See: https://alg.li/lNiZZ7` + ); + } + return queryID; +}; + +const getPositions = (selectedHits: Hit[]): number[] => + selectedHits.map((hit) => hit.__position); + +export const inferPayload = ({ + method, + results, + hits, + objectIDs, +}: { + method: InsightsClientMethod; + results: SearchResults; + hits: Hit[]; + objectIDs: string[]; +}): Omit => { + const { index } = results; + const selectedHits = getSelectedHits(hits, objectIDs); + const queryID = getQueryID(selectedHits); + + switch (method) { + case 'clickedObjectIDsAfterSearch': { + const positions = getPositions(selectedHits); + return { index, queryID, objectIDs, positions }; + } + + case 'convertedObjectIDsAfterSearch': + return { index, queryID, objectIDs }; + + default: + throw new Error(`Unsupported method passed to insights: "${method}".`); + } +}; + +const wrapInsightsClient = + ( + aa: InsightsClient | null, + results: SearchResults, + hits: Hit[] + ): InsightsClient => + (method, ...payloads) => { + const [payload] = payloads; + warning( + false, + `\`insights\` function has been deprecated. It is still supported in 4.x releases, but not further. It is replaced by the \`insights\` middleware. + +For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/` + ); + if (!aa) { + const withInstantSearchUsage = createDocumentationMessageGenerator({ + name: 'instantsearch', + }); + throw new Error( + withInstantSearchUsage( + 'The `insightsClient` option has not been provided to `instantsearch`.' + ) + ); + } + if (!Array.isArray(payload.objectIDs)) { + throw new TypeError('Expected `objectIDs` to be an array.'); + } + const inferredPayload = inferPayload({ + method, + results, + hits, + objectIDs: payload.objectIDs, + }); + aa(method, { ...inferredPayload, ...payload }); + }; + +/** + * @deprecated This function will be still supported in 4.x releases, but not further. It is replaced by the `insights` middleware. For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/ + * It passes `insights` to `HitsWithInsightsListener` and `InfiniteHitsWithInsightsListener`. + */ +export default function withInsights>( + connector: TConnector +): TConnector { + return ((renderFn, unmountFn) => + connector((renderOptions, isFirstRender) => { + const { results, hits, instantSearchInstance } = renderOptions; + if (results && hits && instantSearchInstance) { + const insights = wrapInsightsClient( + instantSearchInstance.insightsClient, + results, + hits + ); + return renderFn({ ...renderOptions, insights }, isFirstRender); + } + return renderFn(renderOptions, isFirstRender); + }, unmountFn)) as TConnector; +} diff --git a/packages/instantsearch-core/src/lib/insights/index.ts b/packages/instantsearch-core/src/lib/insights/index.ts new file mode 100644 index 00000000000..880fa63eb8c --- /dev/null +++ b/packages/instantsearch-core/src/lib/insights/index.ts @@ -0,0 +1 @@ +export { default as withInsights, inferPayload as inferInsightsPayload } from './client'; diff --git a/packages/instantsearch-core/src/lib/routers/history.ts b/packages/instantsearch-core/src/lib/routers/history.ts new file mode 100644 index 00000000000..4c7f34baefa --- /dev/null +++ b/packages/instantsearch-core/src/lib/routers/history.ts @@ -0,0 +1,371 @@ +import qs from 'qs'; + +import { createDocumentationLink, safelyRunOnBrowser, warning } from '../utils'; + +import type { Router, UiState } from '../../types'; + +type CreateURL = (args: { + qsModule: typeof qs; + routeState: TRouteState; + location: Location; +}) => string; + +type ParseURL = (args: { + qsModule: typeof qs; + location: Location; +}) => TRouteState; + +export type BrowserHistoryArgs = { + windowTitle?: (routeState: TRouteState) => string; + writeDelay: number; + createURL: CreateURL; + parseURL: ParseURL; + // @MAJOR: The `Location` type is hard to simulate in non-browser environments + // so we should accept a subset of it that is easier to work with in any + // environments. + getLocation: () => Location; + start?: (onUpdate: () => void) => void; + dispose?: () => void; + push?: (url: string) => void; + /** + * Whether the URL should be cleaned up when the router is disposed. + * This can be useful when closing a modal containing InstantSearch, to + * remove active refinements from the URL. + * @default true + */ + // @MAJOR: Switch the default to `false` and remove the console info in the next major version. + cleanUrlOnDispose?: boolean; +}; + +const setWindowTitle = (title?: string): void => { + if (title) { + // This function is only executed on browsers so we can disable this check. + // eslint-disable-next-line no-restricted-globals + window.document.title = title; + } +}; + +class BrowserHistory implements Router { + public $$type = 'ais.browser'; + /** + * Transforms a UI state into a title for the page. + */ + private readonly windowTitle?: BrowserHistoryArgs['windowTitle']; + /** + * Time in milliseconds before performing a write in the history. + * It prevents from adding too many entries in the history and + * makes the back button more usable. + * + * @default 400 + */ + private readonly writeDelay: Required< + BrowserHistoryArgs + >['writeDelay']; + /** + * Creates a full URL based on the route state. + * The storage adaptor maps all syncable keys to the query string of the URL. + */ + private readonly _createURL: Required< + BrowserHistoryArgs + >['createURL']; + /** + * Parses the URL into a route state. + * It should be symmetrical to `createURL`. + */ + private readonly parseURL: Required< + BrowserHistoryArgs + >['parseURL']; + /** + * Returns the location to store in the history. + * @default () => window.location + */ + private readonly getLocation: Required< + BrowserHistoryArgs + >['getLocation']; + + private writeTimer?: ReturnType; + private _onPopState?: (event: PopStateEvent) => void; + + /** + * Indicates if last action was back/forward in the browser. + */ + private inPopState: boolean = false; + + /** + * Indicates whether the history router is disposed or not. + */ + protected isDisposed: boolean = false; + + /** + * Indicates the window.history.length before the last call to + * window.history.pushState (called in `write`). + * It allows to determine if a `pushState` has been triggered elsewhere, + * and thus to prevent the `write` method from calling `pushState`. + */ + private latestAcknowledgedHistory: number = 0; + + private _start?: (onUpdate: () => void) => void; + private _dispose?: () => void; + private _push?: (url: string) => void; + private _cleanUrlOnDispose: boolean; + + /** + * Initializes a new storage provider that syncs the search state to the URL + * using web APIs (`window.location.pushState` and `onpopstate` event). + */ + public constructor({ + windowTitle, + writeDelay = 400, + createURL, + parseURL, + getLocation, + start, + dispose, + push, + cleanUrlOnDispose, + }: BrowserHistoryArgs) { + this.windowTitle = windowTitle; + this.writeTimer = undefined; + this.writeDelay = writeDelay; + this._createURL = createURL; + this.parseURL = parseURL; + this.getLocation = getLocation; + this._start = start; + this._dispose = dispose; + this._push = push; + this._cleanUrlOnDispose = + typeof cleanUrlOnDispose === 'undefined' ? true : cleanUrlOnDispose; + + if (__DEV__ && typeof cleanUrlOnDispose === 'undefined') { + // eslint-disable-next-line no-console + console.info(`Starting from the next major version, InstantSearch will not clean up the URL from active refinements when it is disposed. + +We recommend setting \`cleanUrlOnDispose\` to false to adopt this change today. +To stay with the current behaviour and remove this warning, set the option to true. + +See documentation: ${createDocumentationLink({ + name: 'history-router', + })}#widget-param-cleanurlondispose`); + } + + safelyRunOnBrowser(({ window: browserWindow }) => { + const title = this.windowTitle && this.windowTitle(this.read()); + setWindowTitle(title); + + this.latestAcknowledgedHistory = browserWindow.history.length; + }); + } + + /** + * Reads the URL and returns a syncable UI search state. + */ + public read(): TRouteState { + return this.parseURL({ qsModule: qs, location: this.getLocation() }); + } + + /** + * Pushes a search state into the URL. + */ + public write(routeState: TRouteState): void { + safelyRunOnBrowser(({ window: browserWindow }) => { + const url = this.createURL(routeState); + const title = this.windowTitle && this.windowTitle(routeState); + + if (this.writeTimer) { + clearTimeout(this.writeTimer); + } + + this.writeTimer = setTimeout(() => { + setWindowTitle(title); + + if (this.shouldWrite(url)) { + if (this._push) { + this._push(url); + } else { + browserWindow.history.pushState(routeState, title || '', url); + } + this.latestAcknowledgedHistory = browserWindow.history.length; + } + this.inPopState = false; + this.writeTimer = undefined; + }, this.writeDelay); + }); + } + + /** + * Sets a callback on the `onpopstate` event of the history API of the current page. + * It enables the URL sync to keep track of the changes. + */ + public onUpdate(callback: (routeState: TRouteState) => void): void { + if (this._start) { + this._start(() => { + callback(this.read()); + }); + } + + this._onPopState = () => { + if (this.writeTimer) { + clearTimeout(this.writeTimer); + this.writeTimer = undefined; + } + + this.inPopState = true; + + // We always read the state from the URL because the state of the history + // can be incorect in some cases (e.g. using React Router). + callback(this.read()); + }; + + safelyRunOnBrowser(({ window: browserWindow }) => { + browserWindow.addEventListener('popstate', this._onPopState!); + }); + } + + /** + * Creates a complete URL from a given syncable UI state. + * + * It always generates the full URL, not a relative one. + * This allows to handle cases like using a . + * See: https://github.com/algolia/instantsearch/issues/790 + */ + public createURL(routeState: TRouteState): string { + const url = this._createURL({ + qsModule: qs, + routeState, + location: this.getLocation(), + }); + + if (__DEV__) { + try { + // We just want to check if the URL is valid. + // eslint-disable-next-line no-new + new URL(url); + } catch (e) { + warning( + false, + `The URL returned by the \`createURL\` function is invalid. +Please make sure it returns an absolute URL to avoid issues, e.g: \`https://algolia.com/search?query=iphone\`.` + ); + } + } + + return url; + } + + /** + * Removes the event listener and cleans up the URL. + */ + public dispose(): void { + if (this._dispose) { + this._dispose(); + } + + this.isDisposed = true; + + safelyRunOnBrowser(({ window: browserWindow }) => { + if (this._onPopState) { + browserWindow.removeEventListener('popstate', this._onPopState); + } + }); + + if (this.writeTimer) { + clearTimeout(this.writeTimer); + } + + if (this._cleanUrlOnDispose) { + this.write({} as TRouteState); + } + } + + public start() { + this.isDisposed = false; + } + + private shouldWrite(url: string): boolean { + return safelyRunOnBrowser(({ window: browserWindow }) => { + // When disposed and the cleanUrlOnDispose is set to false, we do not want to write the URL. + if (this.isDisposed && !this._cleanUrlOnDispose) { + return false; + } + + // We do want to `pushState` if: + // - the router is not disposed, IS.js needs to update the URL + // OR + // - the last write was from InstantSearch.js + // (unlike a SPA, where it would have last written) + const lastPushWasByISAfterDispose = !( + this.isDisposed && + this.latestAcknowledgedHistory !== browserWindow.history.length + ); + + return ( + // When the last state change was through popstate, the IS.js state changes, + // but that should not write the URL. + !this.inPopState && + // When the previous pushState after dispose was by IS.js, we want to write the URL. + lastPushWasByISAfterDispose && + // When the URL is the same as the current one, we do not want to write it. + url !== browserWindow.location.href + ); + }); + } +} + +export default function historyRouter({ + createURL = ({ qsModule, routeState, location }) => { + const { protocol, hostname, port = '', pathname, hash } = location; + const queryString = qsModule.stringify(routeState); + const portWithPrefix = port === '' ? '' : `:${port}`; + + // IE <= 11 has no proper `location.origin` so we cannot rely on it. + if (!queryString) { + return `${protocol}//${hostname}${portWithPrefix}${pathname}${hash}`; + } + + return `${protocol}//${hostname}${portWithPrefix}${pathname}?${queryString}${hash}`; + }, + parseURL = ({ qsModule, location }) => { + // `qs` by default converts arrays with more than 20 items to an object. + // We want to avoid this because the data structure manipulated can therefore vary. + // Setting the limit to `100` seems a good number because the engine's default is 100 + // (it can go up to 1000 but it is very unlikely to select more than 100 items in the UI). + // + // Using an `arrayLimit` of `n` allows `n + 1` items. + // + // See: + // - https://github.com/ljharb/qs#parsing-arrays + // - https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/ + return qsModule.parse(location.search.slice(1), { + arrayLimit: 99, + }) as unknown as TRouteState; + }, + writeDelay = 400, + windowTitle, + getLocation = () => { + return safelyRunOnBrowser( + ({ window: browserWindow }) => browserWindow.location, + { + fallback: () => { + throw new Error( + 'You need to provide `getLocation` to the `history` router in environments where `window` does not exist.' + ); + }, + }); + }, + start, + dispose, + push, + cleanUrlOnDispose, +}: Partial> = {}): BrowserHistory { + return new BrowserHistory({ + createURL, + parseURL, + writeDelay, + windowTitle, + getLocation, + start, + dispose, + push, + cleanUrlOnDispose, + }); +} diff --git a/packages/instantsearch-core/src/lib/routers/index.ts b/packages/instantsearch-core/src/lib/routers/index.ts new file mode 100644 index 00000000000..344a7fccbce --- /dev/null +++ b/packages/instantsearch-core/src/lib/routers/index.ts @@ -0,0 +1,3 @@ +export { default as history } from './history'; +export { default as historyRouter } from './history'; +export type * from './history'; diff --git a/packages/instantsearch-core/src/lib/server.ts b/packages/instantsearch-core/src/lib/server.ts new file mode 100644 index 00000000000..99cfce33fc4 --- /dev/null +++ b/packages/instantsearch-core/src/lib/server.ts @@ -0,0 +1,150 @@ +import { walkIndex } from './utils'; + +import type { + SearchClient, + CompositionClient, + IndexWidget, + InitialResults, + InstantSearch, + SearchOptions, +} from '../types'; + +/** + * Waits for the results from the search instance to coordinate the next steps + * in `getServerState()`. + */ +export function waitForResults( + search: InstantSearch, + skipRecommend: boolean = false +): Promise { + const helper = search.mainHelper!; + + // Extract search parameters from the search client to use them + // later during hydration. + let requestParamsList: SearchOptions[]; + const client = helper.getClient(); + if (search.compositionID) { + helper.setClient({ + ...client, + search(query) { + requestParamsList = [query.requestBody.params]; + return (client as CompositionClient).search(query); + }, + } as CompositionClient); + } else { + helper.setClient({ + ...client, + search(queries) { + requestParamsList = queries.map(({ params }) => params); + return (client as SearchClient).search(queries); + }, + } as SearchClient); + } + + if (search._hasSearchWidget) { + if (search.compositionID) { + helper.searchWithComposition(); + } else { + helper.searchOnlyWithDerivedHelpers(); + } + } + !skipRecommend && search._hasRecommendWidget && helper.recommend(); + + return new Promise((resolve, reject) => { + let searchResultsReceived = !search._hasSearchWidget; + let recommendResultsReceived = !search._hasRecommendWidget || skipRecommend; + // All derived helpers resolve in the same tick so we're safe only relying + // on the first one. + helper.derivedHelpers[0].on('result', () => { + searchResultsReceived = true; + if (recommendResultsReceived) { + resolve(requestParamsList!); + } + }); + helper.derivedHelpers[0].on('recommend:result', () => { + recommendResultsReceived = true; + if (searchResultsReceived) { + resolve(requestParamsList!); + } + }); + + // However, we listen to errors that can happen on any derived helper because + // any error is critical. + helper.on('error', (error) => { + reject(error); + }); + search.on('error', (error) => { + reject(error); + }); + helper.derivedHelpers.forEach((derivedHelper) => + derivedHelper.on('error', (error) => { + reject(error); + }) + ); + }); +} + +/** + * Walks the InstantSearch root index to construct the initial results. + */ +export function getInitialResults( + rootIndex: IndexWidget, + /** + * Search parameters sent to the search client, + * returned by `waitForResults()`. + */ + requestParamsList?: SearchOptions[] +): InitialResults { + const initialResults: InitialResults = {}; + + let requestParamsIndex = 0; + walkIndex(rootIndex, (widget) => { + const searchResults = widget.getResults(); + const recommendResults = widget.getHelper()?.lastRecommendResults; + if (searchResults || recommendResults) { + const resultsCount = searchResults?._rawResults?.length || 0; + const requestParams = resultsCount + ? requestParamsList?.slice( + requestParamsIndex, + requestParamsIndex + resultsCount + ) + : []; + requestParamsIndex += resultsCount; + initialResults[widget.getIndexId()] = { + // We convert the Helper state to a plain object to pass parsable data + // structures from server to client. + ...(searchResults && { + state: { + ...searchResults._state, + clickAnalytics: requestParams?.[0]?.clickAnalytics, + userToken: requestParams?.[0]?.userToken, + }, + results: searchResults._rawResults, + ...(searchResults.feeds && + searchResults.feeds.length > 0 && { + compositionFeedsResults: searchResults.feeds.map((feed) => ({ + ...feed._rawResults[0], + feedID: feed.feedID, + })), + }), + }), + ...(recommendResults && { + recommendResults: { + // We have to stringify + parse because of some explicitly undefined values. + params: JSON.parse(JSON.stringify(recommendResults._state.params)), + results: recommendResults._rawResults, + }, + }), + ...(requestParams && { requestParams }), + }; + } + }); + + if (Object.keys(initialResults).length === 0) { + throw new Error( + 'The root index does not have any results. Make sure you have at least one widget that provides results.' + ); + } + + return initialResults; +} diff --git a/packages/instantsearch-core/src/lib/stateMappings/index.ts b/packages/instantsearch-core/src/lib/stateMappings/index.ts new file mode 100644 index 00000000000..208987fef25 --- /dev/null +++ b/packages/instantsearch-core/src/lib/stateMappings/index.ts @@ -0,0 +1,4 @@ +export { default as simple } from './simple'; +export { default as simpleStateMapping } from './simple'; +export { default as singleIndex } from './singleIndex'; +export { default as singleIndexStateMapping } from './singleIndex'; diff --git a/packages/instantsearch-core/src/lib/stateMappings/simple.ts b/packages/instantsearch-core/src/lib/stateMappings/simple.ts new file mode 100644 index 00000000000..3ddc3a21a3f --- /dev/null +++ b/packages/instantsearch-core/src/lib/stateMappings/simple.ts @@ -0,0 +1,45 @@ +import type { UiState, IndexUiState, StateMapping } from '../../types'; + +function getIndexStateWithoutConfigure( + uiState: TIndexUiState +): Omit { + const { configure, ...trackedUiState } = uiState; + return trackedUiState; +} + +// technically a URL could contain any key, since users provide it, +// which is why the input to this function is UiState, not something +// which excludes "configure" as this function does. +export default function simpleStateMapping< + TUiState extends UiState = UiState +>(): StateMapping { + return { + $$type: 'ais.simple', + + stateToRoute(uiState) { + return Object.keys(uiState).reduce( + (state, indexId) => ({ + ...state, + [indexId]: getIndexStateWithoutConfigure(uiState[indexId]), + }), + {} as TUiState + ); + }, + + routeToState(routeState = {} as TUiState) { + return Object.keys(routeState).reduce( + (state, indexId) => { + const indexState = routeState[indexId]; + if (typeof indexState !== 'object' || indexState === null) { + return state; + } + return { + ...state, + [indexId]: getIndexStateWithoutConfigure(indexState), + }; + }, + {} as TUiState + ); + }, + }; +} diff --git a/packages/instantsearch-core/src/lib/stateMappings/singleIndex.ts b/packages/instantsearch-core/src/lib/stateMappings/singleIndex.ts new file mode 100644 index 00000000000..0ad943877fb --- /dev/null +++ b/packages/instantsearch-core/src/lib/stateMappings/singleIndex.ts @@ -0,0 +1,26 @@ +import type { StateMapping, IndexUiState, UiState } from '../../types'; + +function getIndexStateWithoutConfigure( + uiState: TIndexUiState +): TIndexUiState { + const { configure, ...trackedUiState } = uiState; + return trackedUiState as TIndexUiState; +} + +export default function singleIndexStateMapping< + TUiState extends UiState = UiState +>( + indexName: keyof TUiState +): StateMapping { + return { + $$type: 'ais.singleIndex', + stateToRoute(uiState) { + return getIndexStateWithoutConfigure(uiState[indexName] || {}); + }, + routeToState(routeState = {} as TUiState[typeof indexName]) { + return { + [indexName]: getIndexStateWithoutConfigure(routeState), + } as unknown as TUiState; + }, + }; +} diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/capitalize-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/capitalize-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/capitalize-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/capitalize-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/clearRefinements-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/clearRefinements-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/clearRefinements-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/clearRefinements-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/concatHighlightedParts-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/concatHighlightedParts-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/concatHighlightedParts-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/concatHighlightedParts-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/debounce-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/debounce-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/debounce-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/debounce-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/defer.ts b/packages/instantsearch-core/src/lib/utils/__tests__/defer-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/defer.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/defer-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/escape-highlight-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/escape-highlight-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/escape-highlight-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/escape-highlight-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/escape-html-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/escape-html-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/escape-html-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/escape-html-test.ts diff --git a/packages/instantsearch-ui-components/src/lib/utils/__tests__/find-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/find-test.ts similarity index 100% rename from packages/instantsearch-ui-components/src/lib/utils/__tests__/find-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/find-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/findIndex-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/findIndex-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/findIndex-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/findIndex-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/flat-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/flat-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/flat-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/flat-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/geo-search-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/geo-search-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/geo-search-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/geo-search-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getAlgoliaAgent-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/getAlgoliaAgent-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/getAlgoliaAgent-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/getAlgoliaAgent-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getAppIdAndApiKey-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/getAppIdAndApiKey-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/getAppIdAndApiKey-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/getAppIdAndApiKey-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getHighlightFromSiblings-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/getHighlightFromSiblings-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/getHighlightFromSiblings-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/getHighlightFromSiblings-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getHighlightedParts-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/getHighlightedParts-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/getHighlightedParts-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/getHighlightedParts-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getObjectType-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/getObjectType-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/getObjectType-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/getObjectType-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getPropertyByPath-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/getPropertyByPath-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/getPropertyByPath-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/getPropertyByPath-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getRefinements-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/getRefinements-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/getRefinements-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/getRefinements-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/hits-absolute-position-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/hits-absolute-position-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/hits-absolute-position-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/hits-absolute-position-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/hits-query-id-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/hits-query-id-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/hits-query-id-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/hits-query-id-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/hydrateRecommendCache-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/hydrateRecommendCache-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/hydrateRecommendCache-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/hydrateRecommendCache-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-composition-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/hydrateSearchClient-composition-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-composition-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/hydrateSearchClient-composition-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/hydrateSearchClient-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/hydrateSearchClient-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/isEqual-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/isEqual-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/isEqual-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/isEqual-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/isPlainObject-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/isPlainObject-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/isPlainObject-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/isPlainObject-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/isTwoPassWidget.test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/isTwoPassWidget-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/isTwoPassWidget.test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/isTwoPassWidget-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/logger-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/logger-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/logger-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/logger-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/mergeSearchParameters-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/mergeSearchParameters-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/mergeSearchParameters-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/mergeSearchParameters-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/noop-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/noop-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/noop-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/noop-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/range-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/range-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/range-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/range-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/reverseHighlightedParts-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/reverseHighlightedParts-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/reverseHighlightedParts-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/reverseHighlightedParts-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/safelyRunOnBrowser-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/safelyRunOnBrowser-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/safelyRunOnBrowser-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/safelyRunOnBrowser-test.ts diff --git a/packages/instantsearch-ui-components/src/lib/utils/__tests__/startsWith-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/startsWith-test.ts similarity index 100% rename from packages/instantsearch-ui-components/src/lib/utils/__tests__/startsWith-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/startsWith-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/toArray-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/toArray-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/toArray-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/toArray-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/uniq-test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/uniq-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/uniq-test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/uniq-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/walkIndex.test.ts b/packages/instantsearch-core/src/lib/utils/__tests__/walkIndex-test.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/__tests__/walkIndex.test.ts rename to packages/instantsearch-core/src/lib/utils/__tests__/walkIndex-test.ts diff --git a/packages/instantsearch.js/src/lib/utils/addWidgetId.ts b/packages/instantsearch-core/src/lib/utils/addWidgetId.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/addWidgetId.ts rename to packages/instantsearch-core/src/lib/utils/addWidgetId.ts diff --git a/packages/instantsearch.js/src/lib/utils/capitalize.ts b/packages/instantsearch-core/src/lib/utils/capitalize.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/capitalize.ts rename to packages/instantsearch-core/src/lib/utils/capitalize.ts diff --git a/packages/instantsearch-core/src/lib/utils/chatMessage.ts b/packages/instantsearch-core/src/lib/utils/chatMessage.ts new file mode 100644 index 00000000000..6fd8beff6dd --- /dev/null +++ b/packages/instantsearch-core/src/lib/utils/chatMessage.ts @@ -0,0 +1,45 @@ +import { startsWith } from './startsWith'; + +import type { UIMessage } from '../ai-lite'; + + +type ChatToolMessage = Extract< + UIMessage['parts'][number], + { type: `tool-${string}` } +>; + +export const getTextContent = (message: UIMessage) => { + return message.parts + .map((part) => ('text' in part ? part.text : '')) + .join(''); +}; + +export const hasTextContent = (message: UIMessage) => { + return getTextContent(message).trim() !== ''; +}; + +export const isPartText = ( + part: UIMessage['parts'][number] +): part is Extract => { + return part.type === 'text'; +}; + +export const isPartTool = ( + part: UIMessage['parts'][number] +): part is ChatToolMessage => { + return startsWith(part.type, 'tool-'); +}; + +export function findTool( + partType: string, + tools: Record +): TTool | undefined { + const toolName = partType.replace('tool-', ''); + let tool: TTool | undefined = tools[toolName]; + if (!tool) { + tool = Object.entries(tools).find(([key]) => + startsWith(toolName, `${key}_`) + )?.[1]; + } + return tool; +} diff --git a/packages/instantsearch.js/src/lib/utils/checkIndexUiState.ts b/packages/instantsearch-core/src/lib/utils/checkIndexUiState.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/checkIndexUiState.ts rename to packages/instantsearch-core/src/lib/utils/checkIndexUiState.ts diff --git a/packages/instantsearch.js/src/lib/utils/checkRendering.ts b/packages/instantsearch-core/src/lib/utils/checkRendering.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/checkRendering.ts rename to packages/instantsearch-core/src/lib/utils/checkRendering.ts diff --git a/packages/instantsearch.js/src/lib/utils/clearRefinements.ts b/packages/instantsearch-core/src/lib/utils/clearRefinements.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/clearRefinements.ts rename to packages/instantsearch-core/src/lib/utils/clearRefinements.ts diff --git a/packages/instantsearch.js/src/lib/utils/concatHighlightedParts.ts b/packages/instantsearch-core/src/lib/utils/concatHighlightedParts.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/concatHighlightedParts.ts rename to packages/instantsearch-core/src/lib/utils/concatHighlightedParts.ts diff --git a/packages/instantsearch.js/src/lib/utils/createConcurrentSafePromise.ts b/packages/instantsearch-core/src/lib/utils/createConcurrentSafePromise.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/createConcurrentSafePromise.ts rename to packages/instantsearch-core/src/lib/utils/createConcurrentSafePromise.ts diff --git a/packages/instantsearch.js/src/lib/utils/createSendEventForFacet.ts b/packages/instantsearch-core/src/lib/utils/createSendEventForFacet.ts similarity index 97% rename from packages/instantsearch.js/src/lib/utils/createSendEventForFacet.ts rename to packages/instantsearch-core/src/lib/utils/createSendEventForFacet.ts index 952afbac1e8..d009410cb61 100644 --- a/packages/instantsearch.js/src/lib/utils/createSendEventForFacet.ts +++ b/packages/instantsearch-core/src/lib/utils/createSendEventForFacet.ts @@ -1,6 +1,6 @@ import { isFacetRefined } from './isFacetRefined'; -import type { InstantSearch } from '../../types'; +import type InstantSearch from '../../instantsearch'; import type { AlgoliaSearchHelper } from 'algoliasearch-helper'; type BuiltInSendEventForFacet = ( diff --git a/packages/instantsearch.js/src/lib/utils/createSendEventForHits.ts b/packages/instantsearch-core/src/lib/utils/createSendEventForHits.ts similarity index 98% rename from packages/instantsearch.js/src/lib/utils/createSendEventForHits.ts rename to packages/instantsearch-core/src/lib/utils/createSendEventForHits.ts index adf5fcca9b7..f7bfeee90f1 100644 --- a/packages/instantsearch.js/src/lib/utils/createSendEventForHits.ts +++ b/packages/instantsearch-core/src/lib/utils/createSendEventForHits.ts @@ -1,7 +1,8 @@ import { serializePayload } from './serializer'; +import type InstantSearch from '../../instantsearch'; import type { InsightsEvent } from '../../middlewares/createInsightsMiddleware'; -import type { InstantSearch, Hit, EscapedHits } from '../../types'; +import type { Hit, EscapedHits } from '../../types'; import type { AlgoliaSearchHelper } from 'algoliasearch-helper'; type BuiltInSendEventForHits = ( diff --git a/packages/instantsearch.js/src/lib/utils/debounce.ts b/packages/instantsearch-core/src/lib/utils/debounce.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/debounce.ts rename to packages/instantsearch-core/src/lib/utils/debounce.ts diff --git a/packages/instantsearch.js/src/lib/utils/defer.ts b/packages/instantsearch-core/src/lib/utils/defer.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/defer.ts rename to packages/instantsearch-core/src/lib/utils/defer.ts diff --git a/packages/instantsearch.js/src/lib/utils/documentation.ts b/packages/instantsearch-core/src/lib/utils/documentation.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/documentation.ts rename to packages/instantsearch-core/src/lib/utils/documentation.ts diff --git a/packages/instantsearch.js/src/lib/utils/escape-highlight.ts b/packages/instantsearch-core/src/lib/utils/escape-highlight.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/escape-highlight.ts rename to packages/instantsearch-core/src/lib/utils/escape-highlight.ts diff --git a/packages/instantsearch.js/src/lib/utils/escape-html.ts b/packages/instantsearch-core/src/lib/utils/escape-html.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/escape-html.ts rename to packages/instantsearch-core/src/lib/utils/escape-html.ts diff --git a/packages/instantsearch.js/src/lib/utils/escapeFacetValue.ts b/packages/instantsearch-core/src/lib/utils/escapeFacetValue.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/escapeFacetValue.ts rename to packages/instantsearch-core/src/lib/utils/escapeFacetValue.ts diff --git a/packages/instantsearch.js/src/lib/utils/find.ts b/packages/instantsearch-core/src/lib/utils/find.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/find.ts rename to packages/instantsearch-core/src/lib/utils/find.ts diff --git a/packages/instantsearch.js/src/lib/utils/findIndex.ts b/packages/instantsearch-core/src/lib/utils/findIndex.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/findIndex.ts rename to packages/instantsearch-core/src/lib/utils/findIndex.ts diff --git a/packages/instantsearch.js/src/lib/utils/flat.ts b/packages/instantsearch-core/src/lib/utils/flat.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/flat.ts rename to packages/instantsearch-core/src/lib/utils/flat.ts diff --git a/packages/instantsearch.js/src/lib/utils/geo-search.ts b/packages/instantsearch-core/src/lib/utils/geo-search.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/geo-search.ts rename to packages/instantsearch-core/src/lib/utils/geo-search.ts diff --git a/packages/instantsearch.js/src/lib/utils/getAlgoliaAgent.ts b/packages/instantsearch-core/src/lib/utils/getAlgoliaAgent.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getAlgoliaAgent.ts rename to packages/instantsearch-core/src/lib/utils/getAlgoliaAgent.ts diff --git a/packages/instantsearch.js/src/lib/utils/getAppIdAndApiKey.ts b/packages/instantsearch-core/src/lib/utils/getAppIdAndApiKey.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getAppIdAndApiKey.ts rename to packages/instantsearch-core/src/lib/utils/getAppIdAndApiKey.ts diff --git a/packages/instantsearch.js/src/lib/utils/getHighlightFromSiblings.ts b/packages/instantsearch-core/src/lib/utils/getHighlightFromSiblings.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getHighlightFromSiblings.ts rename to packages/instantsearch-core/src/lib/utils/getHighlightFromSiblings.ts diff --git a/packages/instantsearch.js/src/lib/utils/getHighlightedParts.ts b/packages/instantsearch-core/src/lib/utils/getHighlightedParts.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getHighlightedParts.ts rename to packages/instantsearch-core/src/lib/utils/getHighlightedParts.ts diff --git a/packages/instantsearch.js/src/lib/utils/getObjectType.ts b/packages/instantsearch-core/src/lib/utils/getObjectType.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getObjectType.ts rename to packages/instantsearch-core/src/lib/utils/getObjectType.ts diff --git a/packages/instantsearch.js/src/lib/utils/getPropertyByPath.ts b/packages/instantsearch-core/src/lib/utils/getPropertyByPath.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getPropertyByPath.ts rename to packages/instantsearch-core/src/lib/utils/getPropertyByPath.ts diff --git a/packages/instantsearch.js/src/lib/utils/getRefinements.ts b/packages/instantsearch-core/src/lib/utils/getRefinements.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getRefinements.ts rename to packages/instantsearch-core/src/lib/utils/getRefinements.ts diff --git a/packages/instantsearch.js/src/lib/utils/getWidgetAttribute.ts b/packages/instantsearch-core/src/lib/utils/getWidgetAttribute.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/getWidgetAttribute.ts rename to packages/instantsearch-core/src/lib/utils/getWidgetAttribute.ts diff --git a/packages/instantsearch.js/src/lib/utils/hits-absolute-position.ts b/packages/instantsearch-core/src/lib/utils/hits-absolute-position.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/hits-absolute-position.ts rename to packages/instantsearch-core/src/lib/utils/hits-absolute-position.ts diff --git a/packages/instantsearch.js/src/lib/utils/hits-query-id.ts b/packages/instantsearch-core/src/lib/utils/hits-query-id.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/hits-query-id.ts rename to packages/instantsearch-core/src/lib/utils/hits-query-id.ts diff --git a/packages/instantsearch.js/src/lib/utils/hydrateRecommendCache.ts b/packages/instantsearch-core/src/lib/utils/hydrateRecommendCache.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/hydrateRecommendCache.ts rename to packages/instantsearch-core/src/lib/utils/hydrateRecommendCache.ts diff --git a/packages/instantsearch.js/src/lib/utils/hydrateSearchClient.ts b/packages/instantsearch-core/src/lib/utils/hydrateSearchClient.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/hydrateSearchClient.ts rename to packages/instantsearch-core/src/lib/utils/hydrateSearchClient.ts diff --git a/packages/instantsearch-core/src/lib/utils/index.ts b/packages/instantsearch-core/src/lib/utils/index.ts new file mode 100644 index 00000000000..ce0cf21766d --- /dev/null +++ b/packages/instantsearch-core/src/lib/utils/index.ts @@ -0,0 +1,58 @@ +export * from './addWidgetId'; +export * from './capitalize'; +export * from './chatMessage'; +export * from './checkIndexUiState'; +export * from './checkRendering'; +export * from './clearRefinements'; +export * from './concatHighlightedParts'; +export * from './createConcurrentSafePromise'; +export * from './createSendEventForFacet'; +export * from './createSendEventForHits'; +export * from './debounce'; +export * from './defer'; +export * from './documentation'; +export * from './escape-highlight'; +export * from './escape-html'; +export * from './escapeFacetValue'; +export * from './find'; +export * from './findIndex'; +export * from './flat'; +export * from './geo-search'; +export * from './getAlgoliaAgent'; +export * from './getAppIdAndApiKey'; +export * from './getHighlightFromSiblings'; +export * from './getHighlightedParts'; +export * from './getObjectType'; +export * from './getPropertyByPath'; +export * from './getRefinements'; +export * from './getWidgetAttribute'; +export * from './hits-absolute-position'; +export * from './hits-query-id'; +export * from './hydrateRecommendCache'; +export * from './hydrateSearchClient'; +export * from './isEqual'; +export * from './isFacetRefined'; +export * from './isFiniteNumber'; +export * from './isIndexWidget'; +export * from './isPlainObject'; +export * from './isSpecialClick'; +export * from './isTwoPassWidget'; +export * from './logger'; +export * from './mergeSearchParameters'; +export * from './noop'; +export * from './omit'; +export * from './promptSuggestions'; +export * from './range'; +export * from './render-args'; +export * from './resolveSearchParameters'; +export * from './reverseHighlightedParts'; +export * from './safelyRunOnBrowser'; +export * from './sendChatMessageFeedback'; +export * from './serializer'; +export * from './setIndexHelperState'; +export * from './startsWith'; +export * from './toArray'; +export * from './typedObject'; +export * from './uniq'; +export * from './uuid'; +export * from './walkIndex'; diff --git a/packages/instantsearch.js/src/lib/utils/isEqual.ts b/packages/instantsearch-core/src/lib/utils/isEqual.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/isEqual.ts rename to packages/instantsearch-core/src/lib/utils/isEqual.ts diff --git a/packages/instantsearch.js/src/lib/utils/isFacetRefined.ts b/packages/instantsearch-core/src/lib/utils/isFacetRefined.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/isFacetRefined.ts rename to packages/instantsearch-core/src/lib/utils/isFacetRefined.ts diff --git a/packages/instantsearch.js/src/lib/utils/isFiniteNumber.ts b/packages/instantsearch-core/src/lib/utils/isFiniteNumber.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/isFiniteNumber.ts rename to packages/instantsearch-core/src/lib/utils/isFiniteNumber.ts diff --git a/packages/instantsearch.js/src/lib/utils/isIndexWidget.ts b/packages/instantsearch-core/src/lib/utils/isIndexWidget.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/isIndexWidget.ts rename to packages/instantsearch-core/src/lib/utils/isIndexWidget.ts diff --git a/packages/instantsearch.js/src/lib/utils/isPlainObject.ts b/packages/instantsearch-core/src/lib/utils/isPlainObject.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/isPlainObject.ts rename to packages/instantsearch-core/src/lib/utils/isPlainObject.ts diff --git a/packages/instantsearch.js/src/lib/utils/isSpecialClick.ts b/packages/instantsearch-core/src/lib/utils/isSpecialClick.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/isSpecialClick.ts rename to packages/instantsearch-core/src/lib/utils/isSpecialClick.ts diff --git a/packages/instantsearch.js/src/lib/utils/isTwoPassWidget.ts b/packages/instantsearch-core/src/lib/utils/isTwoPassWidget.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/isTwoPassWidget.ts rename to packages/instantsearch-core/src/lib/utils/isTwoPassWidget.ts diff --git a/packages/instantsearch.js/src/lib/utils/logger.ts b/packages/instantsearch-core/src/lib/utils/logger.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/logger.ts rename to packages/instantsearch-core/src/lib/utils/logger.ts diff --git a/packages/instantsearch.js/src/lib/utils/mergeSearchParameters.ts b/packages/instantsearch-core/src/lib/utils/mergeSearchParameters.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/mergeSearchParameters.ts rename to packages/instantsearch-core/src/lib/utils/mergeSearchParameters.ts diff --git a/packages/instantsearch.js/src/lib/utils/noop.ts b/packages/instantsearch-core/src/lib/utils/noop.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/noop.ts rename to packages/instantsearch-core/src/lib/utils/noop.ts diff --git a/packages/instantsearch.js/src/lib/utils/omit.ts b/packages/instantsearch-core/src/lib/utils/omit.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/omit.ts rename to packages/instantsearch-core/src/lib/utils/omit.ts diff --git a/packages/instantsearch-ui-components/src/lib/utils/promptSuggestions.ts b/packages/instantsearch-core/src/lib/utils/promptSuggestions.ts similarity index 100% rename from packages/instantsearch-ui-components/src/lib/utils/promptSuggestions.ts rename to packages/instantsearch-core/src/lib/utils/promptSuggestions.ts diff --git a/packages/instantsearch.js/src/lib/utils/range.ts b/packages/instantsearch-core/src/lib/utils/range.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/range.ts rename to packages/instantsearch-core/src/lib/utils/range.ts diff --git a/packages/instantsearch.js/src/lib/utils/render-args.ts b/packages/instantsearch-core/src/lib/utils/render-args.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/render-args.ts rename to packages/instantsearch-core/src/lib/utils/render-args.ts diff --git a/packages/instantsearch.js/src/lib/utils/resolveSearchParameters.ts b/packages/instantsearch-core/src/lib/utils/resolveSearchParameters.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/resolveSearchParameters.ts rename to packages/instantsearch-core/src/lib/utils/resolveSearchParameters.ts diff --git a/packages/instantsearch.js/src/lib/utils/reverseHighlightedParts.ts b/packages/instantsearch-core/src/lib/utils/reverseHighlightedParts.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/reverseHighlightedParts.ts rename to packages/instantsearch-core/src/lib/utils/reverseHighlightedParts.ts diff --git a/packages/instantsearch.js/src/lib/utils/safelyRunOnBrowser.ts b/packages/instantsearch-core/src/lib/utils/safelyRunOnBrowser.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/safelyRunOnBrowser.ts rename to packages/instantsearch-core/src/lib/utils/safelyRunOnBrowser.ts diff --git a/packages/instantsearch.js/src/lib/utils/sendChatMessageFeedback.ts b/packages/instantsearch-core/src/lib/utils/sendChatMessageFeedback.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/sendChatMessageFeedback.ts rename to packages/instantsearch-core/src/lib/utils/sendChatMessageFeedback.ts diff --git a/packages/instantsearch.js/src/lib/utils/serializer.ts b/packages/instantsearch-core/src/lib/utils/serializer.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/serializer.ts rename to packages/instantsearch-core/src/lib/utils/serializer.ts diff --git a/packages/instantsearch.js/src/lib/utils/setIndexHelperState.ts b/packages/instantsearch-core/src/lib/utils/setIndexHelperState.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/setIndexHelperState.ts rename to packages/instantsearch-core/src/lib/utils/setIndexHelperState.ts diff --git a/packages/instantsearch-ui-components/src/lib/utils/startsWith.ts b/packages/instantsearch-core/src/lib/utils/startsWith.ts similarity index 100% rename from packages/instantsearch-ui-components/src/lib/utils/startsWith.ts rename to packages/instantsearch-core/src/lib/utils/startsWith.ts diff --git a/packages/instantsearch.js/src/lib/utils/toArray.ts b/packages/instantsearch-core/src/lib/utils/toArray.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/toArray.ts rename to packages/instantsearch-core/src/lib/utils/toArray.ts diff --git a/packages/instantsearch.js/src/lib/utils/typedObject.ts b/packages/instantsearch-core/src/lib/utils/typedObject.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/typedObject.ts rename to packages/instantsearch-core/src/lib/utils/typedObject.ts diff --git a/packages/instantsearch.js/src/lib/utils/uniq.ts b/packages/instantsearch-core/src/lib/utils/uniq.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/uniq.ts rename to packages/instantsearch-core/src/lib/utils/uniq.ts diff --git a/packages/instantsearch.js/src/lib/utils/uuid.ts b/packages/instantsearch-core/src/lib/utils/uuid.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/uuid.ts rename to packages/instantsearch-core/src/lib/utils/uuid.ts diff --git a/packages/instantsearch.js/src/lib/utils/walkIndex.ts b/packages/instantsearch-core/src/lib/utils/walkIndex.ts similarity index 100% rename from packages/instantsearch.js/src/lib/utils/walkIndex.ts rename to packages/instantsearch-core/src/lib/utils/walkIndex.ts diff --git a/packages/instantsearch-core/src/lib/voiceSearchHelper/index.ts b/packages/instantsearch-core/src/lib/voiceSearchHelper/index.ts new file mode 100644 index 00000000000..f33514b4a93 --- /dev/null +++ b/packages/instantsearch-core/src/lib/voiceSearchHelper/index.ts @@ -0,0 +1,131 @@ +// `SpeechRecognition` is an API used on the browser so we can safely disable +// the `window` check. +/* eslint-disable no-restricted-globals */ +/* global SpeechRecognition SpeechRecognitionEvent */ +import type { + CreateVoiceSearchHelper, + Status, + VoiceListeningState, +} from './types'; + +const createVoiceSearchHelper: CreateVoiceSearchHelper = + function createVoiceSearchHelper({ + searchAsYouSpeak, + language, + onQueryChange, + onStateChange, + }) { + const SpeechRecognitionAPI: new () => SpeechRecognition = + (window as any).webkitSpeechRecognition || + (window as any).SpeechRecognition; + const getDefaultState = (status: Status): VoiceListeningState => ({ + status, + transcript: '', + isSpeechFinal: false, + errorCode: undefined, + }); + let state: VoiceListeningState = getDefaultState('initial'); + let recognition: SpeechRecognition | undefined; + + const isBrowserSupported = (): boolean => Boolean(SpeechRecognitionAPI); + + const isListening = (): boolean => + state.status === 'askingPermission' || + state.status === 'waiting' || + state.status === 'recognizing'; + + const setState = (newState: Partial = {}): void => { + state = { ...state, ...newState }; + onStateChange(); + }; + + const getState = (): VoiceListeningState => state; + + const resetState = (status: Status = 'initial'): void => { + setState(getDefaultState(status)); + }; + + const onStart = (): void => { + setState({ + status: 'waiting', + }); + }; + + const onError = (event: Event): void => { + setState({ status: 'error', errorCode: (event as any).error }); + }; + + const onResult = (event: SpeechRecognitionEvent): void => { + setState({ + status: 'recognizing', + transcript: + (event.results[0] && + event.results[0][0] && + event.results[0][0].transcript) || + '', + isSpeechFinal: event.results[0] && event.results[0].isFinal, + }); + if (searchAsYouSpeak && state.transcript) { + onQueryChange(state.transcript); + } + }; + + const onEnd = (): void => { + if (!state.errorCode && state.transcript && !searchAsYouSpeak) { + onQueryChange(state.transcript); + } + if (state.status !== 'error') { + setState({ status: 'finished' }); + } + }; + + const startListening = (): void => { + recognition = new SpeechRecognitionAPI(); + if (!recognition) { + return; + } + resetState('askingPermission'); + recognition.interimResults = true; + + if (language) { + recognition.lang = language; + } + + recognition.addEventListener('start', onStart); + recognition.addEventListener('error', onError); + recognition.addEventListener('result', onResult); + recognition.addEventListener('end', onEnd); + recognition.start(); + }; + + const dispose = (): void => { + if (!recognition) { + return; + } + recognition.stop(); + recognition.removeEventListener('start', onStart); + recognition.removeEventListener('error', onError); + recognition.removeEventListener('result', onResult); + recognition.removeEventListener('end', onEnd); + recognition = undefined; + }; + + const stopListening = (): void => { + dispose(); + // Because `dispose` removes event listeners, `end` listener is not called. + // So we're setting the `status` as `finished` here. + // If we don't do it, it will be still `waiting` or `recognizing`. + resetState('finished'); + }; + + return { + getState, + isBrowserSupported, + isListening, + startListening, + stopListening, + dispose, + }; + }; + +export default createVoiceSearchHelper; diff --git a/packages/instantsearch.js/src/lib/voiceSearchHelper/types.ts b/packages/instantsearch-core/src/lib/voiceSearchHelper/types.ts similarity index 100% rename from packages/instantsearch.js/src/lib/voiceSearchHelper/types.ts rename to packages/instantsearch-core/src/lib/voiceSearchHelper/types.ts diff --git a/packages/instantsearch.js/src/middlewares/createInsightsMiddleware.ts b/packages/instantsearch-core/src/middlewares/createInsightsMiddleware.ts similarity index 100% rename from packages/instantsearch.js/src/middlewares/createInsightsMiddleware.ts rename to packages/instantsearch-core/src/middlewares/createInsightsMiddleware.ts diff --git a/packages/instantsearch.js/src/middlewares/createMetadataMiddleware.ts b/packages/instantsearch-core/src/middlewares/createMetadataMiddleware.ts similarity index 96% rename from packages/instantsearch.js/src/middlewares/createMetadataMiddleware.ts rename to packages/instantsearch-core/src/middlewares/createMetadataMiddleware.ts index 9f41696b349..c4036dd8fa0 100644 --- a/packages/instantsearch.js/src/middlewares/createMetadataMiddleware.ts +++ b/packages/instantsearch-core/src/middlewares/createMetadataMiddleware.ts @@ -5,12 +5,8 @@ import { safelyRunOnBrowser, } from '../lib/utils'; -import type { - InstantSearch, - InternalMiddleware, - Widget, - IndexWidget, -} from '../types'; +import type InstantSearch from '../instantsearch'; +import type { InternalMiddleware, Widget, IndexWidget } from '../types'; type WidgetMetadata = | { diff --git a/packages/instantsearch.js/src/middlewares/createRouterMiddleware.ts b/packages/instantsearch-core/src/middlewares/createRouterMiddleware.ts similarity index 100% rename from packages/instantsearch.js/src/middlewares/createRouterMiddleware.ts rename to packages/instantsearch-core/src/middlewares/createRouterMiddleware.ts diff --git a/packages/instantsearch-core/src/middlewares/index.ts b/packages/instantsearch-core/src/middlewares/index.ts new file mode 100644 index 00000000000..51d8b18c38d --- /dev/null +++ b/packages/instantsearch-core/src/middlewares/index.ts @@ -0,0 +1,3 @@ +export * from './createInsightsMiddleware'; +export * from './createMetadataMiddleware'; +export * from './createRouterMiddleware'; diff --git a/packages/instantsearch.js/src/types/algoliasearch.ts b/packages/instantsearch-core/src/types/algoliasearch.ts similarity index 100% rename from packages/instantsearch.js/src/types/algoliasearch.ts rename to packages/instantsearch-core/src/types/algoliasearch.ts diff --git a/packages/instantsearch.js/src/types/connector.ts b/packages/instantsearch-core/src/types/connector.ts similarity index 97% rename from packages/instantsearch.js/src/types/connector.ts rename to packages/instantsearch-core/src/types/connector.ts index 2e285219adf..723f5cf87e5 100644 --- a/packages/instantsearch.js/src/types/connector.ts +++ b/packages/instantsearch-core/src/types/connector.ts @@ -1,5 +1,5 @@ +import type InstantSearch from '../instantsearch'; import type { InsightsClient } from './insights'; -import type { InstantSearch } from './instantsearch'; import type { Hit } from './results'; import type { UnknownWidgetParams, Widget, WidgetDescription } from './widget'; import type { SearchResults } from 'algoliasearch-helper'; diff --git a/packages/instantsearch-core/src/types/index.ts b/packages/instantsearch-core/src/types/index.ts new file mode 100644 index 00000000000..5d0466f347c --- /dev/null +++ b/packages/instantsearch-core/src/types/index.ts @@ -0,0 +1,17 @@ +export * from './algoliasearch'; +export * from './connector'; +export * from './insights'; +export type { default as InstantSearch } from '../instantsearch'; +export type { + InstantSearchOptions, + InstantSearchStatus, +} from '../instantsearch'; +export * from './middleware'; +export * from './recommend'; +export * from './render-state'; +export * from './results'; +export * from './router'; +export * from './ui-state'; +export * from './utils'; +export * from './widget'; +export * from './widget-factory'; diff --git a/packages/instantsearch.js/src/types/insights.ts b/packages/instantsearch-core/src/types/insights.ts similarity index 100% rename from packages/instantsearch.js/src/types/insights.ts rename to packages/instantsearch-core/src/types/insights.ts diff --git a/packages/instantsearch.js/src/types/middleware.ts b/packages/instantsearch-core/src/types/middleware.ts similarity index 95% rename from packages/instantsearch.js/src/types/middleware.ts rename to packages/instantsearch-core/src/types/middleware.ts index 4806a7494e1..1d23aeffe89 100644 --- a/packages/instantsearch.js/src/types/middleware.ts +++ b/packages/instantsearch-core/src/types/middleware.ts @@ -1,4 +1,4 @@ -import type InstantSearch from '../lib/InstantSearch'; +import type InstantSearch from '../instantsearch'; import type { UiState } from './ui-state'; import type { AtLeastOne } from './utils'; diff --git a/packages/instantsearch.js/src/types/recommend.ts b/packages/instantsearch-core/src/types/recommend.ts similarity index 100% rename from packages/instantsearch.js/src/types/recommend.ts rename to packages/instantsearch-core/src/types/recommend.ts diff --git a/packages/instantsearch.js/src/types/render-state.ts b/packages/instantsearch-core/src/types/render-state.ts similarity index 100% rename from packages/instantsearch.js/src/types/render-state.ts rename to packages/instantsearch-core/src/types/render-state.ts diff --git a/packages/instantsearch.js/src/types/results.ts b/packages/instantsearch-core/src/types/results.ts similarity index 100% rename from packages/instantsearch.js/src/types/results.ts rename to packages/instantsearch-core/src/types/results.ts diff --git a/packages/instantsearch.js/src/types/router.ts b/packages/instantsearch-core/src/types/router.ts similarity index 100% rename from packages/instantsearch.js/src/types/router.ts rename to packages/instantsearch-core/src/types/router.ts diff --git a/packages/instantsearch.js/src/types/ui-state.ts b/packages/instantsearch-core/src/types/ui-state.ts similarity index 100% rename from packages/instantsearch.js/src/types/ui-state.ts rename to packages/instantsearch-core/src/types/ui-state.ts diff --git a/packages/instantsearch.js/src/types/utils.ts b/packages/instantsearch-core/src/types/utils.ts similarity index 100% rename from packages/instantsearch.js/src/types/utils.ts rename to packages/instantsearch-core/src/types/utils.ts diff --git a/packages/instantsearch-core/src/types/widget-factory.ts b/packages/instantsearch-core/src/types/widget-factory.ts new file mode 100644 index 00000000000..f55c13f7aee --- /dev/null +++ b/packages/instantsearch-core/src/types/widget-factory.ts @@ -0,0 +1,21 @@ +import type { UnknownWidgetParams, Widget, WidgetDescription } from './widget'; + +/** + * The function that creates a new widget. + */ +export type WidgetFactory< + TWidgetDescription extends WidgetDescription, + TConnectorParams extends UnknownWidgetParams, + TWidgetParams extends UnknownWidgetParams +> = ( + /** + * The params of the widget. + */ + widgetParams: TWidgetParams & TConnectorParams +) => Widget< + TWidgetDescription & { + widgetParams: TConnectorParams; + } +>; + +export type UnknownWidgetFactory = WidgetFactory<{ $$type: string }, any, any>; diff --git a/packages/instantsearch.js/src/types/widget.ts b/packages/instantsearch-core/src/types/widget.ts similarity index 99% rename from packages/instantsearch.js/src/types/widget.ts rename to packages/instantsearch-core/src/types/widget.ts index 8271e5397f9..5c2c59c1db0 100644 --- a/packages/instantsearch.js/src/types/widget.ts +++ b/packages/instantsearch-core/src/types/widget.ts @@ -1,6 +1,6 @@ +import type InstantSearch from '../instantsearch'; import type { IndexWidget } from '../widgets'; import type { RecommendResponse } from './algoliasearch'; -import type { InstantSearch } from './instantsearch'; import type { IndexRenderState, WidgetRenderState } from './render-state'; import type { IndexUiState, UiState } from './ui-state'; import type { Expand, RequiredKeys } from './utils'; diff --git a/packages/instantsearch-core/src/version.ts b/packages/instantsearch-core/src/version.ts new file mode 100644 index 00000000000..2f6ce8b5e7d --- /dev/null +++ b/packages/instantsearch-core/src/version.ts @@ -0,0 +1 @@ +export default '0.1.0'; diff --git a/packages/instantsearch-core/src/widgets/analytics/analytics.ts b/packages/instantsearch-core/src/widgets/analytics/analytics.ts new file mode 100644 index 00000000000..4a45e32ba97 --- /dev/null +++ b/packages/instantsearch-core/src/widgets/analytics/analytics.ts @@ -0,0 +1,25 @@ +import type { WidgetRenderState } from '../../types'; +import type { SearchParameters, SearchResults } from 'algoliasearch-helper'; + +export type AnalyticsWidgetParamsPushFunction = ( + formattedParameters: string, + state: SearchParameters, + results: SearchResults +) => void; + +export type AnalyticsWidgetParams = { + pushFunction: AnalyticsWidgetParamsPushFunction; + delay?: number; + triggerOnUIInteraction?: boolean; + pushInitialSearch?: boolean; + pushPagination?: boolean; +}; + +export type AnalyticsWidgetDescription = { + $$type: 'ais.analytics'; + $$widgetType: 'ais.analytics'; + renderState: Record; + indexRenderState: { + analytics: WidgetRenderState, AnalyticsWidgetParams>; + }; +}; diff --git a/packages/instantsearch-core/src/widgets/index.ts b/packages/instantsearch-core/src/widgets/index.ts new file mode 100644 index 00000000000..4d05a82c47e --- /dev/null +++ b/packages/instantsearch-core/src/widgets/index.ts @@ -0,0 +1,2 @@ +export { default as index } from './index/index'; +export type * from './index/index'; diff --git a/packages/instantsearch-core/src/widgets/index/index.ts b/packages/instantsearch-core/src/widgets/index/index.ts new file mode 100644 index 00000000000..c3f9be1b085 --- /dev/null +++ b/packages/instantsearch-core/src/widgets/index/index.ts @@ -0,0 +1,1083 @@ +import algoliasearchHelper from 'algoliasearch-helper'; + +import { + checkIndexUiState, + createDocumentationMessageGenerator, + resolveSearchParameters, + mergeSearchParameters, + warning, + isIndexWidget, + createInitArgs, + createRenderArgs, + storeRenderState, + defer, +} from '../../lib/utils'; +import { addWidgetId } from '../../lib/utils/addWidgetId'; + +import type InstantSearch from '../../instantsearch'; +import type { + UiState, + IndexUiState, + Widget, + ScopedResult, + RenderOptions, + RecommendResponse, + SearchClient, + IndexWidgetType, +} from '../../types'; +import type { + AlgoliaSearchHelper as Helper, + DerivedHelper, + SearchParameters, + SearchResults, + AlgoliaSearchHelper, + RecommendParameters, +} from 'algoliasearch-helper'; + +const withUsage = createDocumentationMessageGenerator({ + name: 'index-widget', +}); + +export type IndexWidgetParams = + | { + /** + * The index or composition id to target. + */ + indexName: string; + /** + * Id to use for the index if there are multiple indices with the same name. + * This will be used to create the URL and the render state. + */ + indexId?: string; + /** + * If `true`, the index will not be merged with the main helper's state. + * This means that the index will not be part of the main search request. + * + * @default false + */ + EXPERIMENTAL_isolated?: false; + } + | { + /** + * If `true`, the index will not be merged with the main helper's state. + * This means that the index will not be part of the main search request. + * + * This option is EXPERIMENTAL, and implementation details may change in the future. + * Things that could change are: + * - which widgets get rendered when a change happens + * - whether the index searches automatically + * - whether the index is included in the URL / UiState + * - whether the index is included in server-side rendering + * + * @default false + */ + EXPERIMENTAL_isolated: true; + /** + * The index or composition id to target. + */ + indexName?: string; + /** + * Id to use for the index if there are multiple indices with the same name. + * This will be used to create the URL and the render state. + */ + indexId?: string; + }; + +export type IndexInitOptions = { + instantSearchInstance: InstantSearch; + parent: IndexWidget | null; + uiState: UiState; +}; + +export type IndexRenderOptions = { + instantSearchInstance: InstantSearch; +}; + +type WidgetSearchParametersOptions = Parameters< + NonNullable +>[1]; +type LocalWidgetSearchParametersOptions = WidgetSearchParametersOptions & { + initialSearchParameters: SearchParameters; +}; +type LocalWidgetRecommendParametersOptions = WidgetSearchParametersOptions & { + initialRecommendParameters: RecommendParameters; +}; + +export type IndexWidgetDescription = { + $$type: IndexWidgetType; + $$widgetType: IndexWidgetType; +}; + +export type IndexWidget = Omit< + Widget, + 'getWidgetUiState' | 'getWidgetState' +> & { + getIndexName: () => string; + getIndexId: () => string; + getHelper: () => Helper | null; + getResults: () => SearchResults | null; + getResultsForWidget: ( + widget: IndexWidget | Widget + ) => SearchResults | RecommendResponse | null; + getPreviousState: () => SearchParameters | null; + getScopedResults: () => ScopedResult[]; + getParent: () => IndexWidget | null; + getWidgets: () => Array; + createURL: ( + nextState: SearchParameters | ((state: IndexUiState) => IndexUiState) + ) => string; + + addWidgets: ( + widgets: Array> + ) => IndexWidget; + removeWidgets: ( + widgets: Array + ) => IndexWidget; + + init: (options: IndexInitOptions) => void; + render: (options: IndexRenderOptions) => void; + dispose: () => void; + /** + * @deprecated + */ + getWidgetState: (uiState: UiState) => UiState; + getWidgetUiState: ( + uiState: TSpecificUiState + ) => TSpecificUiState; + getWidgetSearchParameters: ( + searchParameters: SearchParameters, + searchParametersOptions: { uiState: IndexUiState } + ) => SearchParameters; + /** + * Set this index' UI state back to the state defined by the widgets. + * Can only be called after `init`. + */ + refreshUiState: () => void; + /** + * Set this index' UI state and search. This is the equivalent of calling + * a spread `setUiState` on the InstantSearch instance. + * Can only be called after `init`. + */ + setIndexUiState: ( + indexUiState: + | TUiState[string] + | ((previousIndexUiState: TUiState[string]) => TUiState[string]) + ) => void; + /** + * This index is isolated, meaning it will not be merged with the main + * helper's state. + * @private + */ + _isolated: boolean; + /** + * Schedules a search for this index only. + * @private + */ + scheduleLocalSearch: () => void; +}; + +/** + * This is the same content as helper._change / setState, but allowing for extra + * UiState to be synchronized. + * see: https://github.com/algolia/algoliasearch-helper-js/blob/6b835ffd07742f2d6b314022cce6848f5cfecd4a/src/algoliasearch.helper.js#L1311-L1324 + */ +function privateHelperSetState( + helper: AlgoliaSearchHelper, + { + state, + recommendState, + isPageReset, + _uiState, + }: { + state: SearchParameters; + recommendState: RecommendParameters; + isPageReset?: boolean; + _uiState?: IndexUiState; + } +) { + if (state !== helper.state) { + helper.state = state; + + helper.emit('change', { + state: helper.state, + results: helper.lastResults, + isPageReset, + _uiState, + }); + } + + if (recommendState !== helper.recommendState) { + helper.recommendState = recommendState; + + // eslint-disable-next-line no-warning-comments + // TODO: emit "change" event when events for Recommend are implemented + } +} + +type WidgetUiStateOptions = Parameters< + NonNullable +>[1]; + +function getLocalWidgetsUiState( + widgets: Array, + widgetStateOptions: WidgetUiStateOptions, + initialUiState: IndexUiState = {} +) { + return widgets.reduce((uiState, widget) => { + if (isIndexWidget(widget)) { + return uiState; + } + + if (!widget.getWidgetUiState && !widget.getWidgetState) { + return uiState; + } + + if (widget.getWidgetUiState) { + return widget.getWidgetUiState(uiState, widgetStateOptions); + } + + return widget.getWidgetState!(uiState, widgetStateOptions); + }, initialUiState); +} + +function getLocalWidgetsSearchParameters( + widgets: Array, + widgetSearchParametersOptions: LocalWidgetSearchParametersOptions +): SearchParameters { + const { initialSearchParameters, ...rest } = widgetSearchParametersOptions; + + return widgets.reduce((state, widget) => { + if (!widget.getWidgetSearchParameters || isIndexWidget(widget)) { + return state; + } + + if (widget.dependsOn === 'search' && widget.getWidgetParameters) { + return widget.getWidgetParameters(state, rest); + } + + return widget.getWidgetSearchParameters(state, rest); + }, initialSearchParameters); +} + +function getLocalWidgetsRecommendParameters( + widgets: Array, + widgetRecommendParametersOptions: LocalWidgetRecommendParametersOptions +): RecommendParameters { + const { initialRecommendParameters, ...rest } = + widgetRecommendParametersOptions; + + return widgets.reduce((state, widget) => { + if ( + !isIndexWidget(widget) && + widget.dependsOn === 'recommend' && + widget.getWidgetParameters + ) { + return widget.getWidgetParameters(state, rest); + } + return state; + }, initialRecommendParameters); +} + +function resetPageFromWidgets(widgets: Array): void { + const indexWidgets = widgets.filter(isIndexWidget); + + if (indexWidgets.length === 0) { + return; + } + + indexWidgets.forEach((widget) => { + const widgetHelper = widget.getHelper()!; + + privateHelperSetState(widgetHelper, { + state: widgetHelper.state.resetPage(), + recommendState: widgetHelper.recommendState, + isPageReset: true, + }); + + resetPageFromWidgets(widget.getWidgets()); + }); +} + +function resolveScopedResultsFromWidgets( + widgets: Array +): ScopedResult[] { + const indexWidgets = widgets.filter(isIndexWidget); + + return indexWidgets.reduce((scopedResults, current) => { + return scopedResults.concat( + { + indexId: current.getIndexId(), + results: current.getResults()!, + helper: current.getHelper()!, + }, + ...resolveScopedResultsFromWidgets(current.getWidgets()) + ); + }, []); +} + +const index = (widgetParams: IndexWidgetParams): IndexWidget => { + if ( + widgetParams === undefined || + (widgetParams.indexName === undefined && + !widgetParams.EXPERIMENTAL_isolated) + ) { + throw new Error(withUsage('The `indexName` option is required.')); + } + + // When isolated=true, we use an empty string as the default indexName. + // This is intentional: isolated indices do not require a real index name. + const { + indexName = '', + indexId = indexName, + EXPERIMENTAL_isolated: isolated = false, + } = widgetParams; + + let localWidgets: Array = []; + let localUiState: IndexUiState = {}; + let localInstantSearchInstance: InstantSearch | null = null; + let localParent: IndexWidget | null = null; + let helper: Helper | null = null; + let derivedHelper: DerivedHelper | null = null; + let lastValidSearchParameters: SearchParameters | null = null; + let hasRecommendWidget: boolean = false; + let hasSearchWidget: boolean = false; + + return { + $$type: 'ais.index', + $$widgetType: 'ais.index', + + _isolated: isolated, + + getIndexName() { + return indexName; + }, + + getIndexId() { + return indexId; + }, + + getHelper() { + return helper; + }, + + getResults() { + if (!derivedHelper?.lastResults) return null; + + // To make the UI optimistic, we patch the state to display to the current + // one instead of the one associated with the latest results. + // This means user-driven UI changes (e.g., checked checkbox) are reflected + // immediately instead of waiting for Algolia to respond, regardless of + // the status of the network request. + derivedHelper.lastResults._state = helper!.state; + + return derivedHelper.lastResults; + }, + + getResultsForWidget(widget) { + if ( + widget.dependsOn !== 'recommend' || + isIndexWidget(widget) || + widget.$$id === undefined + ) { + return this.getResults(); + } + + if (!helper?.lastRecommendResults) { + return null; + } + + return helper.lastRecommendResults[widget.$$id]; + }, + + getPreviousState() { + return lastValidSearchParameters; + }, + + getScopedResults() { + const widgetParent = this.getParent(); + let widgetSiblings; + + if (widgetParent) { + widgetSiblings = widgetParent.getWidgets(); + } else if (indexName.length === 0) { + // The widget is the root but has no index name: + // we resolve results from its children index widgets + widgetSiblings = this.getWidgets(); + } else { + // The widget is the root and has an index name: + // we consider itself as the only sibling + widgetSiblings = [this]; + } + + return resolveScopedResultsFromWidgets(widgetSiblings); + }, + + getParent() { + return isolated ? null : localParent; + }, + + createURL( + nextState: SearchParameters | ((state: IndexUiState) => IndexUiState) + ) { + if (typeof nextState === 'function') { + return localInstantSearchInstance!._createURL({ + [indexId]: nextState(localUiState), + }); + } + return localInstantSearchInstance!._createURL({ + [indexId]: getLocalWidgetsUiState(localWidgets, { + searchParameters: nextState, + helper: helper!, + }), + }); + }, + + scheduleLocalSearch: defer(() => { + if (isolated) { + helper?.search(); + } + }), + + getWidgets() { + return localWidgets; + }, + + addWidgets(widgets) { + if (!Array.isArray(widgets)) { + throw new Error( + withUsage('The `addWidgets` method expects an array of widgets.') + ); + } + const flatWidgets = widgets.reduce>( + (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), + [] + ); + + if ( + flatWidgets.some( + (widget) => + typeof widget.init !== 'function' && + typeof widget.render !== 'function' + ) + ) { + throw new Error( + withUsage( + 'The widget definition expects a `render` and/or an `init` method.' + ) + ); + } + + flatWidgets.forEach((widget) => { + widget.parent = this; + if (isIndexWidget(widget)) { + return; + } + + if (localInstantSearchInstance && widget.dependsOn === 'recommend') { + localInstantSearchInstance._hasRecommendWidget = true; + } else if (localInstantSearchInstance) { + localInstantSearchInstance._hasSearchWidget = true; + } else if (widget.dependsOn === 'recommend') { + hasRecommendWidget = true; + } else { + hasSearchWidget = true; + } + + addWidgetId(widget); + }); + + localWidgets = localWidgets.concat(flatWidgets); + if (localInstantSearchInstance && Boolean(flatWidgets.length)) { + privateHelperSetState(helper!, { + state: getLocalWidgetsSearchParameters(localWidgets, { + uiState: localUiState, + initialSearchParameters: helper!.state, + }), + recommendState: getLocalWidgetsRecommendParameters(localWidgets, { + uiState: localUiState, + initialRecommendParameters: helper!.recommendState, + }), + _uiState: localUiState, + }); + + // We compute the render state before calling `init` in a separate loop + // to construct the whole render state object that is then passed to + // `init`. + flatWidgets.forEach((widget) => { + if (widget.getRenderState) { + const renderState = widget.getRenderState( + localInstantSearchInstance!.renderState[this.getIndexId()] || {}, + createInitArgs( + localInstantSearchInstance!, + this, + localInstantSearchInstance!._initialUiState + ) + ); + + storeRenderState({ + renderState, + instantSearchInstance: localInstantSearchInstance!, + parent: this, + }); + } + }); + + flatWidgets.forEach((widget) => { + if (widget.init) { + widget.init( + createInitArgs( + localInstantSearchInstance!, + this, + localInstantSearchInstance!._initialUiState + ) + ); + } + }); + + if (isolated) { + this.scheduleLocalSearch(); + } else { + localInstantSearchInstance.scheduleSearch(); + } + } + + return this; + }, + + removeWidgets(widgets) { + if (!Array.isArray(widgets)) { + throw new Error( + withUsage('The `removeWidgets` method expects an array of widgets.') + ); + } + const flatWidgets = widgets.reduce>( + (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), + [] + ); + + if (flatWidgets.some((widget) => typeof widget.dispose !== 'function')) { + throw new Error( + withUsage('The widget definition expects a `dispose` method.') + ); + } + + localWidgets = localWidgets.filter( + (widget) => flatWidgets.indexOf(widget) === -1 + ); + + localWidgets.forEach((widget) => { + widget.parent = undefined; + if (isIndexWidget(widget)) { + return; + } + + if (localInstantSearchInstance && widget.dependsOn === 'recommend') { + localInstantSearchInstance._hasRecommendWidget = true; + } else if (localInstantSearchInstance) { + localInstantSearchInstance._hasSearchWidget = true; + } else if (widget.dependsOn === 'recommend') { + hasRecommendWidget = true; + } else { + hasSearchWidget = true; + } + }); + + if (localInstantSearchInstance && Boolean(flatWidgets.length)) { + const { cleanedSearchState, cleanedRecommendState } = + flatWidgets.reduce( + (states, widget) => { + // the `dispose` method exists at this point we already assert it + const next = widget.dispose!({ + helper: helper!, + state: states.cleanedSearchState, + recommendState: states.cleanedRecommendState, + parent: this, + }); + + if (next instanceof algoliasearchHelper.RecommendParameters) { + states.cleanedRecommendState = next; + } else if (next) { + states.cleanedSearchState = next; + } + + return states; + }, + { + cleanedSearchState: helper!.state, + cleanedRecommendState: helper!.recommendState, + } + ); + + const newState = localInstantSearchInstance.future + .preserveSharedStateOnUnmount + ? getLocalWidgetsSearchParameters(localWidgets, { + uiState: localUiState, + initialSearchParameters: new algoliasearchHelper.SearchParameters( + { + index: this.getIndexName(), + } + ), + }) + : getLocalWidgetsSearchParameters(localWidgets, { + uiState: getLocalWidgetsUiState(localWidgets, { + searchParameters: cleanedSearchState, + helper: helper!, + }), + initialSearchParameters: cleanedSearchState, + }); + + localUiState = getLocalWidgetsUiState(localWidgets, { + searchParameters: newState, + helper: helper!, + }); + + helper!.setState(newState); + helper!.recommendState = cleanedRecommendState; + + if (localWidgets.length) { + if (isolated) { + this.scheduleLocalSearch(); + } else { + localInstantSearchInstance.scheduleSearch(); + } + } + } + + return this; + }, + + init({ instantSearchInstance, parent, uiState }: IndexInitOptions) { + if (helper !== null) { + // helper is already initialized, therefore we do not need to set up + // any listeners + return; + } + + localInstantSearchInstance = instantSearchInstance; + localParent = parent; + localUiState = uiState[indexId] || {}; + + // The `mainHelper` is already defined at this point. The instance is created + // inside InstantSearch at the `start` method, which occurs before the `init` + // step. + const mainHelper = instantSearchInstance.mainHelper!; + const parameters = getLocalWidgetsSearchParameters(localWidgets, { + uiState: localUiState, + initialSearchParameters: new algoliasearchHelper.SearchParameters({ + index: indexName, + }), + }); + const recommendParameters = getLocalWidgetsRecommendParameters( + localWidgets, + { + uiState: localUiState, + initialRecommendParameters: + new algoliasearchHelper.RecommendParameters(), + } + ); + + // This Helper is only used for state management we do not care about the + // `searchClient`. Only the "main" Helper created at the `InstantSearch` + // level is aware of the client. + helper = algoliasearchHelper( + mainHelper.getClient(), + parameters.index, + parameters + ); + helper.recommendState = recommendParameters; + + // We forward the call to `search` to the "main" instance of the Helper + // which is responsible for managing the queries (it's the only one that is + // aware of the `searchClient`). + helper.search = () => { + if (isolated) { + instantSearchInstance.status = 'loading'; + this.render({ instantSearchInstance }); + return instantSearchInstance.compositionID + ? helper!.searchWithComposition() + : helper!.searchOnlyWithDerivedHelpers(); + } + + if (instantSearchInstance.onStateChange) { + instantSearchInstance.onStateChange({ + uiState: instantSearchInstance.mainIndex.getWidgetUiState({}), + setUiState: (nextState) => + instantSearchInstance.setUiState(nextState, false), + }); + + // We don't trigger a search when controlled because it becomes the + // responsibility of `setUiState`. + return mainHelper; + } + + return mainHelper.search(); + }; + + helper.searchWithoutTriggeringOnStateChange = () => { + return mainHelper.search(); + }; + + // We use the same pattern for the `searchForFacetValues`. + helper.searchForFacetValues = ( + facetName, + facetValue, + maxFacetHits, + userState + ) => { + const state = mergeSearchParameters( + mainHelper.state, + ...resolveSearchParameters(this) + ).setQueryParameters(userState!); + + return mainHelper.searchForFacetValues( + facetName, + facetValue, + maxFacetHits, + state + ); + }; + + const isolatedHelper = indexName + ? helper + : algoliasearchHelper({} as SearchClient, '__empty_index__', {}); + const derivingHelper = isolated + ? isolatedHelper + : nearestIsolatedHelper(parent, mainHelper); + + derivedHelper = derivingHelper.derive( + () => + mergeSearchParameters( + mainHelper.state, + ...resolveSearchParameters(this) + ), + () => this.getHelper()!.recommendState + ); + + const indexInitialResults = + instantSearchInstance._initialResults?.[this.getIndexId()]; + + if (indexInitialResults?.results) { + // We restore the shape of the results provided to the instance to respect + // the helper's structure. + const results = new algoliasearchHelper.SearchResults( + new algoliasearchHelper.SearchParameters(indexInitialResults.state), + indexInitialResults.results + ); + + derivedHelper.lastResults = results; + helper.lastResults = results; + } + + if (indexInitialResults?.recommendResults) { + const recommendResults = new algoliasearchHelper.RecommendResults( + new algoliasearchHelper.RecommendParameters({ + params: indexInitialResults.recommendResults.params, + }), + indexInitialResults.recommendResults.results + ); + derivedHelper.lastRecommendResults = recommendResults; + helper.lastRecommendResults = recommendResults; + } + + // Subscribe to the Helper state changes for the page before widgets + // are initialized. This behavior mimics the original one of the Helper. + // It makes sense to replicate it at the `init` step. We have another + // listener on `change` below, once `init` is done. + helper.on('change', ({ isPageReset }) => { + if (isPageReset) { + resetPageFromWidgets(localWidgets); + } + }); + + derivedHelper.on('search', () => { + // The index does not manage the "staleness" of the search. This is the + // responsibility of the main instance. It does not make sense to manage + // it at the index level because it's either: all of them or none of them + // that are stalled. The queries are performed into a single network request. + instantSearchInstance.scheduleStalledRender(); + + if (__DEV__) { + checkIndexUiState({ index: this, indexUiState: localUiState }); + } + }); + + derivedHelper.on('result', ({ results }) => { + // The index does not render the results it schedules a new render + // to let all the other indices emit their own results. It allows us to + // run the render process in one pass. + instantSearchInstance.scheduleRender(); + + // the derived helper is the one which actually searches, but the helper + // which is exposed e.g. via instance.helper, doesn't search, and thus + // does not have access to lastResults, which it used to in pre-federated + // search behavior. + helper!.lastResults = results; + lastValidSearchParameters = results?._state; + }); + + // eslint-disable-next-line no-warning-comments + // TODO: listen to "result" event when events for Recommend are implemented + derivedHelper.on('recommend:result', ({ recommend }) => { + // The index does not render the results it schedules a new render + // to let all the other indices emit their own results. It allows us to + // run the render process in one pass. + instantSearchInstance.scheduleRender(); + + // the derived helper is the one which actually searches, but the helper + // which is exposed e.g. via instance.helper, doesn't search, and thus + // does not have access to lastRecommendResults. + helper!.lastRecommendResults = recommend.results; + }); + + // We compute the render state before calling `init` in a separate loop + // to construct the whole render state object that is then passed to + // `init`. + localWidgets.forEach((widget) => { + if (widget.getRenderState) { + const renderState = widget.getRenderState( + instantSearchInstance.renderState[this.getIndexId()] || {}, + createInitArgs(instantSearchInstance, this, uiState) + ); + + storeRenderState({ + renderState, + instantSearchInstance, + parent: this, + }); + } + }); + + localWidgets.forEach((widget) => { + warning( + // if it has NO getWidgetState or if it has getWidgetUiState, we don't warn + // aka we warn if there's _only_ getWidgetState + !widget.getWidgetState || Boolean(widget.getWidgetUiState), + 'The `getWidgetState` method is renamed `getWidgetUiState` and will no longer exist under that name in InstantSearch.js 5.x. Please use `getWidgetUiState` instead.' + ); + + if (widget.init) { + widget.init(createInitArgs(instantSearchInstance, this, uiState)); + } + }); + + // Subscribe to the Helper state changes for the `uiState` once widgets + // are initialized. Until the first render, state changes are part of the + // configuration step. This is mainly for backward compatibility with custom + // widgets. When the subscription happens before the `init` step, the (static) + // configuration of the widget is pushed in the URL. That's what we want to avoid. + // https://github.com/algolia/instantsearch/pull/994/commits/4a672ae3fd78809e213de0368549ef12e9dc9454 + helper.on('change', (event) => { + const { state } = event; + + const _uiState = (event as any)._uiState; + + localUiState = getLocalWidgetsUiState( + localWidgets, + { + searchParameters: state, + helper: helper!, + }, + _uiState || {} + ); + + // We don't trigger an internal change when controlled because it + // becomes the responsibility of `setUiState`. + if (!instantSearchInstance.onStateChange) { + instantSearchInstance.onInternalStateChange(); + } + }); + + if (indexInitialResults) { + // If there are initial results, we're not notified of the next results + // because we don't trigger an initial search. We therefore need to directly + // schedule a render that will render the results injected on the helper. + instantSearchInstance.scheduleRender(); + } + + if (hasRecommendWidget) { + instantSearchInstance._hasRecommendWidget = true; + } + if (hasSearchWidget) { + instantSearchInstance._hasSearchWidget = true; + } + }, + + render({ instantSearchInstance }: IndexRenderOptions) { + // we can't attach a listener to the error event of search, as the error + // then would no longer be thrown for global handlers. + if ( + instantSearchInstance.status === 'error' && + !instantSearchInstance.mainHelper!.hasPendingRequests() && + lastValidSearchParameters + ) { + helper!.setState(lastValidSearchParameters); + } + + // We only render index widgets if there are no results. + // This makes sure `render` is never called with `results` being `null`. + // If it's an isolated index without an index name, we render all widgets, + // as there are no results to display for the isolated index itself. + let widgetsToRender = + this.getResults() || + derivedHelper?.lastRecommendResults || + (isolated && !indexName) + ? localWidgets + : localWidgets.filter((widget) => widget.shouldRender); + + widgetsToRender = widgetsToRender.filter((widget) => { + if (!widget.shouldRender) { + return true; + } + + return widget.shouldRender({ instantSearchInstance }); + }); + + widgetsToRender.forEach((widget) => { + if (widget.getRenderState) { + const renderState = widget.getRenderState( + instantSearchInstance.renderState[this.getIndexId()] || {}, + createRenderArgs( + instantSearchInstance, + this, + widget + ) as RenderOptions + ); + + storeRenderState({ + renderState, + instantSearchInstance, + parent: this, + }); + } + }); + + widgetsToRender.forEach((widget) => { + // At this point, all the variables used below are set. Both `helper` + // and `derivedHelper` have been created at the `init` step. The attribute + // `lastResults` might be `null` though. It's possible that a stalled render + // happens before the result e.g with a dynamically added index the request might + // be delayed. The render is triggered for the complete tree but some parts do + // not have results yet. + + if (widget.render) { + widget.render( + createRenderArgs( + instantSearchInstance, + this, + widget + ) as RenderOptions + ); + } + }); + }, + + dispose() { + localWidgets.forEach((widget) => { + if (widget.dispose && helper) { + // The dispose function is always called once the instance is started + // (it's an effect of `removeWidgets`). The index is initialized and + // the Helper is available. We don't care about the return value of + // `dispose` because the index is removed. We can't call `removeWidgets` + // because we want to keep the widgets on the instance, to allow idempotent + // operations on `add` & `remove`. + widget.dispose({ + helper, + state: helper.state, + recommendState: helper.recommendState, + parent: this, + }); + } + }); + + localInstantSearchInstance = null; + localParent = null; + helper?.removeAllListeners(); + helper = null; + + derivedHelper?.detach(); + derivedHelper = null; + }, + + getWidgetUiState(uiState: TUiState) { + return localWidgets + .filter(isIndexWidget) + .filter((w) => !w._isolated) + .reduce( + (previousUiState, innerIndex) => + innerIndex.getWidgetUiState(previousUiState), + { + ...uiState, + [indexId]: { + ...uiState[indexId], + ...localUiState, + }, + } + ); + }, + + getWidgetState(uiState: UiState) { + warning( + false, + 'The `getWidgetState` method is renamed `getWidgetUiState` and will no longer exist under that name in InstantSearch.js 5.x. Please use `getWidgetUiState` instead.' + ); + + return this.getWidgetUiState(uiState); + }, + + getWidgetSearchParameters(searchParameters, { uiState }) { + return getLocalWidgetsSearchParameters(localWidgets, { + uiState, + initialSearchParameters: searchParameters, + }); + }, + + shouldRender() { + return true; + }, + + refreshUiState() { + localUiState = getLocalWidgetsUiState( + localWidgets, + { + searchParameters: this.getHelper()!.state, + helper: this.getHelper()!, + }, + localUiState + ); + }, + + setIndexUiState( + indexUiState: + | TIndexUiState + | ((previousIndexUiState: TIndexUiState) => TIndexUiState) + ) { + const nextIndexUiState = + typeof indexUiState === 'function' + ? indexUiState(localUiState as TIndexUiState) + : indexUiState; + + localInstantSearchInstance!.setUiState((state) => ({ + ...state, + [indexId]: nextIndexUiState, + })); + }, + }; +}; + +export default index; + +/** + * Walk up the parent chain to find the closest isolated index, or fall back to mainHelper + */ +function nearestIsolatedHelper( + current: IndexWidget | null, + mainHelper: Helper +): Helper { + while (current) { + if (current._isolated) { + return current.getHelper()!; + } + current = current.getParent(); + } + return mainHelper; +} diff --git a/packages/instantsearch-core/src/widgets/places/places.ts b/packages/instantsearch-core/src/widgets/places/places.ts new file mode 100644 index 00000000000..1085c9686bc --- /dev/null +++ b/packages/instantsearch-core/src/widgets/places/places.ts @@ -0,0 +1,18 @@ +import type { WidgetRenderState } from '../../types'; + +export type PlacesWidgetParams = any; + +export type PlacesWidgetDescription = { + $$type: 'ais.places'; + $$widgetType: 'ais.places'; + renderState: Record; + indexRenderState: { + places: WidgetRenderState, PlacesWidgetParams>; + }; + indexUiState: { + places: { + query: string; + position: string; + }; + }; +}; diff --git a/packages/instantsearch.js/test/createFeedsTestHelpers.ts b/packages/instantsearch-core/test/createFeedsTestHelpers.ts similarity index 100% rename from packages/instantsearch.js/test/createFeedsTestHelpers.ts rename to packages/instantsearch-core/test/createFeedsTestHelpers.ts diff --git a/packages/instantsearch.js/test/createInstantSearch.ts b/packages/instantsearch-core/test/createInstantSearch.ts similarity index 95% rename from packages/instantsearch.js/test/createInstantSearch.ts rename to packages/instantsearch-core/test/createInstantSearch.ts index bc7fb1d3c3b..0a2019b3629 100644 --- a/packages/instantsearch.js/test/createInstantSearch.ts +++ b/packages/instantsearch-core/test/createInstantSearch.ts @@ -1,11 +1,11 @@ import { createSearchClient } from '@instantsearch/mocks'; import algoliasearchHelper from 'algoliasearch-helper'; -import { INSTANTSEARCH_FUTURE_DEFAULTS } from '../src/lib/InstantSearch'; +import { INSTANTSEARCH_FUTURE_DEFAULTS } from '../src/instantsearch'; import { defer } from '../src/lib/utils'; import { index } from '../src/widgets'; -import type { InstantSearch } from '../src/types'; +import type InstantSearch from '../src/instantsearch'; export const createInstantSearch = ( args: Partial = {} diff --git a/packages/instantsearch.js/test/createWidget.ts b/packages/instantsearch-core/test/createWidget.ts similarity index 100% rename from packages/instantsearch.js/test/createWidget.ts rename to packages/instantsearch-core/test/createWidget.ts diff --git a/packages/instantsearch-core/tsconfig.declaration.json b/packages/instantsearch-core/tsconfig.declaration.json new file mode 100644 index 00000000000..74426f7472d --- /dev/null +++ b/packages/instantsearch-core/tsconfig.declaration.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.declaration", + "exclude": ["**/__tests__/**/*", "**/dist/**/*", "test/**/*"] +} diff --git a/packages/instantsearch-ui-components/package.json b/packages/instantsearch-ui-components/package.json index 17729d2a54d..2bb93c13b88 100644 --- a/packages/instantsearch-ui-components/package.json +++ b/packages/instantsearch-ui-components/package.json @@ -44,6 +44,7 @@ "watch:es": "rollup -c rollup.config.mjs --watch" }, "dependencies": { + "instantsearch-core": "0.1.0", "@swc/helpers": "0.5.18", "markdown-to-jsx": "^7.7.15" } diff --git a/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts b/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts index e450aecbff3..0977d36a774 100644 --- a/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts +++ b/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts @@ -1,4 +1,4 @@ -import { find } from '../../lib'; +import { find } from 'instantsearch-core'; import type { ComponentProps, Hooks, MutableRef } from '../../types'; diff --git a/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompleteStorage.ts b/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompleteStorage.ts index ae8a15f765a..f5d5cb057f2 100644 --- a/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompleteStorage.ts +++ b/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompleteStorage.ts @@ -1,4 +1,4 @@ -import { find } from '../../lib'; +import { find } from 'instantsearch-core'; import type { Hooks } from '../../types'; import type { UsePropGetters } from './createAutocompletePropGetters'; diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx index f65c4d1d2b9..e7878093086 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx @@ -1,19 +1,24 @@ /** @jsx createElement */ +import { startsWith } from 'instantsearch-core'; import { compiler } from 'markdown-to-jsx'; -import { cx, startsWith } from '../../lib'; +import { cx } from '../../lib'; import { createButtonComponent } from '../Button'; import { MenuIcon } from './icons'; import type { ComponentProps, Renderer, VNode } from '../../types'; +import type { ClientSideTools } from './types'; import type { AddToolResultWithOutput, - ChatMessageBase, ChatStatus, - ChatToolMessage, - ClientSideTools, -} from './types'; + UIMessage, +} from 'instantsearch-core'; + +type ChatToolMessage = Extract< + UIMessage['parts'][number], + { type: `tool-${string}` } +>; export type ChatMessageSide = 'left' | 'right'; export type ChatMessageVariant = 'neutral' | 'subtle'; @@ -76,14 +81,14 @@ export type ChatMessageActionProps = { /** * Click handler for the action */ - onClick?: (message: ChatMessageBase) => void; + onClick?: (message: UIMessage) => void; }; export type ChatMessageProps = ComponentProps<'article'> & { /** * The message object associated with this chat message */ - message: ChatMessageBase; + message: UIMessage; /** * The status of the message (e.g. whether it's still streaming) */ @@ -113,7 +118,7 @@ export type ChatMessageProps = ComponentProps<'article'> & { */ actionsComponent?: (props: { actions: ChatMessageActionProps[]; - message: ChatMessageBase; + message: UIMessage; }) => JSX.Element | null; /** * Footer content @@ -132,7 +137,7 @@ export type ChatMessageProps = ComponentProps<'article'> & { * receive object IDs (e.g. display results) can hydrate records from a * preceding search tool's hits. */ - messages?: ChatMessageBase[]; + messages?: UIMessage[]; /** * Close the chat */ @@ -166,7 +171,7 @@ export type ChatMessageProps = ComponentProps<'article'> & { parseMarkdown?: boolean; }; -// Keep in sync with packages/instantsearch.js/src/lib/chat/index.ts +// Keep in sync with packages/instantsearch-core/src/lib/chat/index.ts const SearchIndexToolType = 'algolia_search_index'; export function createChatMessageComponent({ createElement }: Renderer) { @@ -223,7 +228,7 @@ export function createChatMessageComponent({ createElement }: Renderer) { }; function renderMessagePart( - part: ChatMessageBase['parts'][number], + part: UIMessage['parts'][number], index: number ) { if (part.type === 'step-start') { diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx index 27539b9b53b..a29dfb05fc9 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx @@ -1,13 +1,14 @@ /** @jsx createElement */ -import { cx } from '../../lib'; import { findTool, getTextContent, hasTextContent, isPartText, isPartTool, -} from '../../lib/utils/chat'; +} from 'instantsearch-core'; + +import { cx } from '../../lib'; import { createButtonComponent } from '../Button'; import { createChatMessageComponent } from './ChatMessage'; @@ -38,13 +39,8 @@ import type { } from './ChatMessage'; import type { ChatMessageErrorProps } from './ChatMessageError'; import type { ChatMessageLoaderProps } from './ChatMessageLoader'; -import type { - ChatEmptyProps, - ChatLayoutOwnProps, - ChatMessageBase, - ChatStatus, - ClientSideTools, -} from './types'; +import type { ChatEmptyProps, ChatLayoutOwnProps, ClientSideTools } from './types'; +import type { ChatStatus, UIMessage } from 'instantsearch-core'; export type ChatMessagesTranslations = { /** @@ -105,7 +101,7 @@ export type ChatMessagesClassNames = { }; export type ChatMessagesProps< - TMessage extends ChatMessageBase = ChatMessageBase + TMessage extends UIMessage = UIMessage > = ComponentProps<'div'> & { /** * Array of messages to display @@ -243,7 +239,7 @@ export type ChatMessagesProps< feedbackState?: Record; }; -const copyToClipboard = (message: ChatMessageBase) => { +const copyToClipboard = (message: UIMessage) => { navigator.clipboard.writeText(getTextContent(message)); }; @@ -259,8 +255,8 @@ const copyToClipboard = (message: ChatMessageBase) => { * `indexUiState` it last rendered with until its next genuine render. */ function areMessagePropsEqual( - prev: { message: ChatMessageBase; [key: string]: unknown }, - next: { message: ChatMessageBase; [key: string]: unknown } + prev: { message: UIMessage; [key: string]: unknown }, + next: { message: UIMessage; [key: string]: unknown } ): boolean { return ( prev.message === next.message && @@ -276,7 +272,7 @@ function areMessagePropsEqual( } function createDefaultMessageComponent< - TMessage extends ChatMessageBase = ChatMessageBase + TMessage extends UIMessage = UIMessage >({ createElement, Fragment }: Renderer) { const ChatMessage = createChatMessageComponent({ createElement, Fragment }); @@ -306,7 +302,7 @@ function createDefaultMessageComponent< assistantMessageProps?: Partial; indexUiState: object; setIndexUiState: (state: object) => void; - messages?: ChatMessageBase[]; + messages?: UIMessage[]; tools: ClientSideTools; onReload: (messageId?: string) => void; onClose: () => void; @@ -368,12 +364,12 @@ function createDefaultMessageComponent< { title: translations.thumbsUpLabel, icon: () => , - onClick: (m: ChatMessageBase) => onFeedback(m.id, 1), + onClick: (m: UIMessage) => onFeedback(m.id, 1), }, { title: translations.thumbsDownLabel, icon: () => , - onClick: (m: ChatMessageBase) => onFeedback(m.id, 0), + onClick: (m: UIMessage) => onFeedback(m.id, 0), } ); } @@ -414,7 +410,7 @@ export function createChatMessagesComponent({ }: Renderer & Pick) { const Button = createButtonComponent({ createElement }); const DefaultMessageComponent = - createDefaultMessageComponent({ createElement, Fragment }); + createDefaultMessageComponent({ createElement, Fragment }); // Skip re-rendering (and re-compiling the markdown of) completed messages on // every streaming delta. const MemoizedDefaultMessage = memo( @@ -429,7 +425,7 @@ export function createChatMessagesComponent({ }); return function ChatMessages< - TMessage extends ChatMessageBase = ChatMessageBase + TMessage extends UIMessage = UIMessage >(userProps: ChatMessagesProps) { const { classNames = {}, @@ -615,7 +611,7 @@ export function createChatMessagesComponent({ const getShowLoader = ( status: ChatStatus, - lastPart: ChatMessageBase['parts'][number] | undefined, + lastPart: UIMessage['parts'][number] | undefined, tools: ClientSideTools ): boolean => { if (status !== 'submitted' && status !== 'streaming') return false; diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatPrompt.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatPrompt.tsx index 7a8a467076c..283cfb39751 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatPrompt.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatPrompt.tsx @@ -6,7 +6,7 @@ import { createButtonComponent } from '../Button'; import { ArrowUpIcon, StopIcon } from './icons'; import type { ComponentProps, MutableRef, Renderer } from '../../types'; -import type { ChatStatus } from './types'; +import type { ChatStatus } from 'instantsearch-core'; export type ChatPromptTranslations = { /** diff --git a/packages/instantsearch-ui-components/src/components/chat/tools/DisplayResultsTool.tsx b/packages/instantsearch-ui-components/src/components/chat/tools/DisplayResultsTool.tsx index d197880b39a..a622d94c67f 100644 --- a/packages/instantsearch-ui-components/src/components/chat/tools/DisplayResultsTool.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/tools/DisplayResultsTool.tsx @@ -1,9 +1,9 @@ /** @jsx createElement */ -import { getHitsByObjectID } from '../../../lib/utils/chat'; +import { getHitsByObjectID } from '../../../lib/chat'; import type { Hooks, RecordWithObjectID, Renderer } from '../../../types'; -import type { ClientSideToolComponentProps } from '../types'; +import type { ClientSideToolComponentProps } from 'instantsearch-core'; export type DisplayResultsTranslations = { /** diff --git a/packages/instantsearch-ui-components/src/components/chat/types.ts b/packages/instantsearch-ui-components/src/components/chat/types.ts index 84b89636c21..98329d2c75f 100644 --- a/packages/instantsearch-ui-components/src/components/chat/types.ts +++ b/packages/instantsearch-ui-components/src/components/chat/types.ts @@ -1,477 +1,25 @@ -import type { ComponentProps, SendEventForHits } from '../../types'; -import type { SearchParameters } from 'algoliasearch-helper'; +import type { ComponentProps } from '../../types'; +import type { + AbstractChat, + ChatState, + ChatStatus, + ClientSideTool as CoreClientSideTool, + ClientSideToolComponentProps, + UIMessage, +} from 'instantsearch-core'; -export type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error'; -export type ChatRole = 'data' | 'user' | 'assistant' | 'system'; - -/** - * Provider metadata type for UI message parts. - */ -export type ProviderMetadata = Record>; - -/** - * A record of data types for data parts in UI messages. - */ -export type UIDataTypes = Record; - -/** - * Tool input/output type definition. - */ -export type UITool = { - input: unknown; - output: unknown | undefined; -}; - -/** - * A record of UI tools. - */ -export type UITools = Record; - -/** - * Helper type to get values of an object. - */ -type ValueOf = T[keyof T]; - -/** - * Deep partial type. - */ -type DeepPartial = T extends object - ? { [P in keyof T]?: DeepPartial } - : T; - -/** - * A text part of a message. - */ -export type TextUIPart = { - type: 'text'; - text: string; - state?: 'streaming' | 'done'; - providerMetadata?: ProviderMetadata; -}; - -/** - * A reasoning part of a message. - */ -export type ReasoningUIPart = { - type: 'reasoning'; - text: string; - state?: 'streaming' | 'done'; - providerMetadata?: ProviderMetadata; -}; - -/** - * A source URL part of a message. - */ -export type SourceUrlUIPart = { - type: 'source-url'; - sourceId: string; - url: string; - title?: string; - providerMetadata?: ProviderMetadata; -}; - -/** - * A document source part of a message. - */ -export type SourceDocumentUIPart = { - type: 'source-document'; - sourceId: string; - mediaType: string; - title: string; - filename?: string; - providerMetadata?: ProviderMetadata; -}; - -/** - * A file part of a message. - */ -export type FileUIPart = { - type: 'file'; - mediaType: string; - filename?: string; - url: string; - providerMetadata?: ProviderMetadata; -}; - -/** - * A step boundary part of a message. - */ -export type StepStartUIPart = { - type: 'step-start'; -}; - -/** - * A data part of a message. - */ -export type DataUIPart = ValueOf<{ - [NAME in keyof TDataTypes & string]: { - type: `data-${NAME}`; - id?: string; - data: TDataTypes[NAME]; - }; -}>; - -/** - * A tool invocation part of a message. - */ -export type ToolUIPart = ValueOf<{ - [NAME in keyof TTools & string]: { - type: `tool-${NAME}`; - toolCallId: string; - } & ( - | { - state: 'input-streaming'; - input: DeepPartial | undefined; - providerExecuted?: boolean; - output?: never; - errorText?: never; - } - | { - state: 'input-available'; - input: TTools[NAME]['input']; - providerExecuted?: boolean; - output?: never; - errorText?: never; - callProviderMetadata?: ProviderMetadata; - } - | { - state: 'output-available'; - input: TTools[NAME]['input']; - output: TTools[NAME]['output']; - errorText?: never; - providerExecuted?: boolean; - callProviderMetadata?: ProviderMetadata; - preliminary?: boolean; - } - | { - state: 'output-error'; - input: TTools[NAME]['input'] | undefined; - rawInput?: unknown; - output?: never; - errorText: string; - providerExecuted?: boolean; - callProviderMetadata?: ProviderMetadata; - } - ); -}>; - -/** - * A dynamic tool invocation part of a message. - */ -export type DynamicToolUIPart = { - type: 'dynamic-tool'; - toolName: string; - toolCallId: string; -} & ( - | { - state: 'input-streaming'; - input: unknown | undefined; - output?: never; - errorText?: never; - } - | { - state: 'input-available'; - input: unknown; - output?: never; - errorText?: never; - callProviderMetadata?: ProviderMetadata; - } - | { - state: 'output-available'; - input: unknown; - output: unknown; - errorText?: never; - callProviderMetadata?: ProviderMetadata; - preliminary?: boolean; - } - | { - state: 'output-error'; - input: unknown; - output?: never; - errorText: string; - callProviderMetadata?: ProviderMetadata; - } -); - -/** - * All possible message part types. - */ -export type UIMessagePart< - TDataTypes extends UIDataTypes = UIDataTypes, - TTools extends UITools = UITools -> = - | TextUIPart - | ReasoningUIPart - | ToolUIPart - | DynamicToolUIPart - | SourceUrlUIPart - | SourceDocumentUIPart - | FileUIPart - | DataUIPart - | StepStartUIPart; - -/** - * AI SDK UI Messages. They are used in the client and to communicate between the frontend and the API routes. - */ -export interface UIMessage< - TMetadata = unknown, - TDataParts extends UIDataTypes = UIDataTypes, - TTools extends UITools = UITools -> { - /** A unique identifier for the message. */ - id: string; - /** The role of the message. */ - role: 'system' | 'user' | 'assistant'; - /** The metadata of the message. */ - metadata?: TMetadata; - /** The parts of the message. Use this for rendering the message in the UI. */ - parts: Array>; -} - -export type ChatMessageBase = UIMessage; - -export type ChatToolMessage = Extract< - ChatMessageBase['parts'][number], - { type: `tool-${string}` } ->; -export type ChatToolType = ChatToolMessage['type']; - -/** - * Infer metadata type from UIMessage. - */ -export type InferUIMessageMetadata = T extends UIMessage< - infer TMetadata -> - ? TMetadata - : unknown; - -/** - * Infer data types from UIMessage. - */ -export type InferUIMessageData = T extends UIMessage< - unknown, - infer TDataTypes -> - ? TDataTypes - : UIDataTypes; - -/** - * Infer tools from UIMessage. - */ -export type InferUIMessageTools = T extends UIMessage< - unknown, - UIDataTypes, - infer TTools -> - ? TTools - : UITools; - -/** - * Chat state interface. - */ -export interface ChatState { - status: ChatStatus; - error: Error | undefined; - messages: TUIMessage[]; - pushMessage: (message: TUIMessage) => void; - popMessage: () => void; - replaceMessage: (index: number, message: TUIMessage) => void; - snapshot: (thing: T) => T; -} - -/** - * ID generator function type. - */ -export type IdGenerator = () => string; - -/** - * Callback function to be called when an error is encountered. - */ -export type ChatOnErrorCallback = (error: Error) => void; - -/** - * Infer tool call type from UIMessage. - */ -export type InferUIMessageToolCall = - | ValueOf<{ - [NAME in keyof InferUIMessageTools]: { - toolName: NAME & string; - toolCallId: string; - input: InferUIMessageTools[NAME] extends { - input: infer INPUT; - } - ? INPUT - : never; - dynamic?: false; - }; - }> - | { - toolName: string; - toolCallId: string; - input: unknown; - dynamic: true; - }; - -/** - * Optional callback function that is invoked when a tool call is received. - */ -export type ChatOnToolCallCallback = - (options: { - toolCall: InferUIMessageToolCall; - }) => void | PromiseLike; - -/** - * Function that is called when the assistant response has finished streaming. - */ -export type ChatOnFinishCallback = (options: { - message: TUIMessage; - messages: TUIMessage[]; - isAbort: boolean; - isDisconnect: boolean; - isError: boolean; -}) => void; - -/** - * Optional callback function that is called when a data part is received. - */ -export type ChatOnDataCallback = ( - dataPart: DataUIPart> -) => void; - -/** - * Transport interface for sending and receiving chat messages. - */ -export interface ChatTransport { - sendMessages: (options: { - chatId: string; - messages: TUIMessage[]; - abortSignal: AbortSignal; - requestMetadata?: unknown; - trigger: 'submit-message' | 'regenerate-message'; - messageId?: string; - }) => Promise>; - - reconnectToStream: (options: { - chatId: string; - }) => Promise | null>; -} - -/** - * Chat initialization options. - */ -export interface ChatInit { - /** A unique identifier for the chat. If not provided, a random one will be generated. */ - id?: string; - messages?: TUIMessage[]; - /** A way to provide a function for generating message and chat IDs. */ - generateId?: IdGenerator; - transport?: ChatTransport; - /** Callback function to be called when an error is encountered. */ - onError?: ChatOnErrorCallback; - /** Optional callback function that is invoked when a tool call is received. */ - onToolCall?: ChatOnToolCallCallback; - /** Function that is called when the assistant response has finished streaming. */ - onFinish?: ChatOnFinishCallback; - /** Optional callback function that is called when a data part is received. */ - onData?: ChatOnDataCallback; - /** - * When provided, this function will be called when the stream is finished or a tool call is added - * to determine if the current messages should be resubmitted. - */ - sendAutomaticallyWhen?: (options: { - messages: TUIMessage[]; - }) => boolean | PromiseLike; -} - -/** - * Abstract base class for chat implementations. - */ -export interface AbstractChat { - readonly id: string; - readonly generateId: IdGenerator; - - status: ChatStatus; - error: Error | undefined; - messages: TUIMessage[]; - lastMessage: TUIMessage | undefined; - - sendMessage: ( - message?: - | (Omit & { - id?: TUIMessage['id']; - role?: TUIMessage['role']; - text?: never; - files?: never; - messageId?: string; - }) - | { - text: string; - files?: FileList | FileUIPart[]; - metadata?: InferUIMessageMetadata; - parts?: never; - messageId?: string; - } - | { - files: FileList | FileUIPart[]; - metadata?: InferUIMessageMetadata; - parts?: never; - messageId?: string; - }, - options?: { headers?: Record | Headers; body?: object } - ) => Promise; - - regenerate: ( - options?: { - messageId?: string; - } & { headers?: Record | Headers; body?: object } - ) => Promise; - - resumeStream: (options?: { - headers?: Record | Headers; - body?: object; - }) => Promise; - - resetConversationId: () => void; - - clearError: () => void; - - addToolResult: >(params: { - tool: TTool; - toolCallId: string; - output: InferUIMessageTools[TTool]['output']; - }) => Promise; - - stop: () => Promise; -} -export type AddToolResult = AbstractChat['addToolResult']; - -export type AddToolResultWithOutput = ( - params: Pick[0], 'output'> -) => ReturnType; - -type SearchToolInputBase = { - query: string; - number_of_results?: number; -}; - -type DefaultSearchToolInput = SearchToolInputBase & { - facet_filters?: string[][]; -}; +export type ClientSideToolComponent = ( + props: ClientSideToolComponentProps +) => JSX.Element; -type McpSearchToolInput = SearchToolInputBase & { - facet_filters?: undefined; - [facetKey: `facet_${string}`]: string[] | undefined; +export type ClientSideTool = Omit & { + layoutComponent?: ClientSideToolComponent; }; -export type SearchToolInput = DefaultSearchToolInput | McpSearchToolInput; - -export type ApplyFiltersParams = { - query?: string; - facetFilters?: string[][]; -}; +export type ClientSideTools = Record; export type ChatLayoutOwnProps< - TMessage extends ChatMessageBase = ChatMessageBase + TMessage extends UIMessage = UIMessage > = { open: boolean; maximized: boolean; @@ -492,43 +40,6 @@ export type ChatLayoutOwnProps< > & ComponentProps<'div'>; -export type ClientSideToolComponentProps = { - message: ChatToolMessage; - messages?: ChatMessageBase[]; - indexUiState: object; - setIndexUiState: (state: object) => void; - onClose: () => void; - addToolResult: AddToolResultWithOutput; - applyFilters: (params: ApplyFiltersParams) => SearchParameters; - sendEvent: SendEventForHits; -}; - -export type ClientSideToolComponent = ( - props: ClientSideToolComponentProps -) => JSX.Element; - -export type ClientSideTool = { - layoutComponent?: ClientSideToolComponent; - streamInput?: boolean; - addToolResult: AddToolResult; - sendEvent?: SendEventForHits; - onToolCall?: ( - params: Parameters< - NonNullable['onToolCall']> - >[0]['toolCall'] & { - addToolResult: AddToolResultWithOutput; - } - ) => void; - applyFilters: (params: ApplyFiltersParams) => SearchParameters; -}; -export type ClientSideTools = Record; - -export type UserClientSideTool = Omit< - ClientSideTool, - 'addToolResult' | 'applyFilters' | 'sendEvent' ->; -export type UserClientSideTools = Record; - export type ChatEmptyProps = { /** * Function to send a message to the chat diff --git a/packages/instantsearch-ui-components/src/lib/utils/__tests__/chat-test.ts b/packages/instantsearch-ui-components/src/lib/__tests__/chat-test.ts similarity index 95% rename from packages/instantsearch-ui-components/src/lib/utils/__tests__/chat-test.ts rename to packages/instantsearch-ui-components/src/lib/__tests__/chat-test.ts index 65cc19dda87..7ac10239e43 100644 --- a/packages/instantsearch-ui-components/src/lib/utils/__tests__/chat-test.ts +++ b/packages/instantsearch-ui-components/src/lib/__tests__/chat-test.ts @@ -1,6 +1,6 @@ import { getFacetFiltersFromToolInput, getHitsByObjectID } from '../chat'; -import type { ChatMessageBase } from '../../../components'; +import type { UIMessage } from 'instantsearch-core'; describe('getFacetFiltersFromToolInput', () => { test('returns undefined when input is undefined', () => { @@ -78,7 +78,7 @@ describe('getFacetFiltersFromToolInput', () => { describe('getHitsByObjectID', () => { test('collects hits from a search tool output keyed by objectID', () => { - const messages: ChatMessageBase[] = [ + const messages: UIMessage[] = [ { id: '1', role: 'assistant', @@ -106,7 +106,7 @@ describe('getHitsByObjectID', () => { }); test('supports the MCP server search tool name shim', () => { - const messages: ChatMessageBase[] = [ + const messages: UIMessage[] = [ { id: '1', role: 'assistant', @@ -128,7 +128,7 @@ describe('getHitsByObjectID', () => { }); test('merges hits across several messages, last write wins per objectID', () => { - const messages: ChatMessageBase[] = [ + const messages: UIMessage[] = [ { id: '1', role: 'assistant', @@ -169,7 +169,7 @@ describe('getHitsByObjectID', () => { }); test('ignores non-search tools, pending states, and invalid hits', () => { - const messages: ChatMessageBase[] = [ + const messages: UIMessage[] = [ { id: '1', role: 'assistant', @@ -199,7 +199,7 @@ describe('getHitsByObjectID', () => { }, ], }, - ] as ChatMessageBase[]; + ] as UIMessage[]; expect(getHitsByObjectID(messages)).toEqual({ 1: { objectID: '1', name: 'Runner' }, @@ -207,7 +207,7 @@ describe('getHitsByObjectID', () => { }); test('scopes collection to the turn containing `untilToolCallId`, ignoring later searches', () => { - const messages: ChatMessageBase[] = [ + const messages: UIMessage[] = [ { id: '1', role: 'assistant', @@ -245,7 +245,7 @@ describe('getHitsByObjectID', () => { }, ], }, - ] as ChatMessageBase[]; + ] as UIMessage[]; // Scoped to the first turn: the later search (with `q2`) must not leak in. expect(getHitsByObjectID(messages, 'display-1')).toEqual({ @@ -259,7 +259,7 @@ describe('getHitsByObjectID', () => { }); test('includes the search in the same message as `untilToolCallId`', () => { - const messages: ChatMessageBase[] = [ + const messages: UIMessage[] = [ { id: '1', role: 'assistant', @@ -280,7 +280,7 @@ describe('getHitsByObjectID', () => { }, ], }, - ] as ChatMessageBase[]; + ] as UIMessage[]; expect(getHitsByObjectID(messages, 'display')).toEqual({ 1: { objectID: '1', name: 'Runner' }, diff --git a/packages/instantsearch-ui-components/src/lib/utils/chat.ts b/packages/instantsearch-ui-components/src/lib/chat.ts similarity index 74% rename from packages/instantsearch-ui-components/src/lib/utils/chat.ts rename to packages/instantsearch-ui-components/src/lib/chat.ts index b1b8c10c1f0..6d2421a39d4 100644 --- a/packages/instantsearch-ui-components/src/lib/utils/chat.ts +++ b/packages/instantsearch-ui-components/src/lib/chat.ts @@ -1,53 +1,16 @@ -import { startsWith } from './startsWith'; +import { isPartTool, startsWith } from 'instantsearch-core'; -import type { ChatMessageBase } from '../../components'; -import type { - ChatToolMessage, - ClientSideTool, - ClientSideTools, - SearchToolInput, -} from '../../components/chat/types'; -import type { RecordWithObjectID } from '../../types'; +import type { SearchToolInput, UIMessage } from 'instantsearch-core'; +import type { RecordWithObjectID } from '../types'; + +type ChatToolMessage = Extract< + UIMessage['parts'][number], + { type: `tool-${string}` } +>; // Keep in sync with packages/instantsearch.js/src/lib/chat/index.ts const SearchIndexToolType = 'algolia_search_index'; -export const getTextContent = (message: ChatMessageBase) => { - return message.parts - .map((part) => ('text' in part ? part.text : '')) - .join(''); -}; - -export const hasTextContent = (message: ChatMessageBase) => { - return getTextContent(message).trim() !== ''; -}; - -export const isPartText = ( - part: ChatMessageBase['parts'][number] -): part is Extract => { - return part.type === 'text'; -}; - -export const isPartTool = ( - part: ChatMessageBase['parts'][number] -): part is ChatToolMessage => { - return startsWith(part.type, 'tool-'); -}; - -export const findTool = ( - partType: string, - tools: ClientSideTools -): ClientSideTool | undefined => { - const toolName = partType.replace('tool-', ''); - let tool: ClientSideTool | undefined = tools[toolName]; - if (!tool) { - tool = Object.entries(tools).find(([key]) => - startsWith(toolName, `${key}_`) - )?.[1]; - } - return tool; -}; - const FACET_KEY_PREFIX = 'facet_'; /** @@ -138,7 +101,7 @@ const collectHitsFromPart = ( * per-query metadata like `__queryID`). */ export const getHitsByObjectID = ( - messages: ChatMessageBase[], + messages: UIMessage[], untilToolCallId?: string ): Record => { const hitsByObjectID: Record = {}; diff --git a/packages/instantsearch-ui-components/src/lib/index.ts b/packages/instantsearch-ui-components/src/lib/index.ts index d5e4b115ecb..1ebe332d76a 100644 --- a/packages/instantsearch-ui-components/src/lib/index.ts +++ b/packages/instantsearch-ui-components/src/lib/index.ts @@ -1,3 +1,3 @@ +export * from './chat'; export * from './cx'; export * from './stickToBottom'; -export * from './utils'; diff --git a/packages/instantsearch-ui-components/src/lib/utils/find.ts b/packages/instantsearch-ui-components/src/lib/utils/find.ts deleted file mode 100644 index c54aad9b1ac..00000000000 --- a/packages/instantsearch-ui-components/src/lib/utils/find.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function find( - array: T[], - predicate: (item: T, i: number, a: T[]) => boolean -): T | undefined { - for (let index = 0; index < array.length; index++) { - const item = array[index]; - if (predicate(item, index, array)) { - return item; - } - } - return undefined; -} diff --git a/packages/instantsearch-ui-components/src/lib/utils/index.ts b/packages/instantsearch-ui-components/src/lib/utils/index.ts deleted file mode 100644 index 977595d5629..00000000000 --- a/packages/instantsearch-ui-components/src/lib/utils/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { getFacetFiltersFromToolInput } from './chat'; -export * from './find'; -export * from './promptSuggestions'; -export * from './startsWith'; diff --git a/packages/instantsearch.js/package.json b/packages/instantsearch.js/package.json index 7ee8287d1ca..1021a5e838a 100644 --- a/packages/instantsearch.js/package.json +++ b/packages/instantsearch.js/package.json @@ -39,7 +39,8 @@ "preact": "^10.10.0", "qs": "^6.5.1", "react": ">= 0.14.0", - "search-insights": "^2.17.2" + "search-insights": "^2.17.2", + "instantsearch-core": "0.1.0" }, "peerDependencies": { "algoliasearch": ">= 3.1 < 6" diff --git a/packages/instantsearch.js/src/components/Breadcrumb/Breadcrumb.tsx b/packages/instantsearch.js/src/components/Breadcrumb/Breadcrumb.tsx index e27e1b306d6..d4d326d28bf 100644 --- a/packages/instantsearch.js/src/components/Breadcrumb/Breadcrumb.tsx +++ b/packages/instantsearch.js/src/components/Breadcrumb/Breadcrumb.tsx @@ -6,7 +6,7 @@ import { h } from 'preact'; import { isSpecialClick } from '../../lib/utils'; import Template from '../Template/Template'; -import type { BreadcrumbConnectorParamsItem } from '../../connectors/breadcrumb/connectBreadcrumb'; +import type { BreadcrumbConnectorParamsItem } from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { ComponentCSSClasses } from '../../types'; import type { diff --git a/packages/instantsearch.js/src/components/ClearRefinements/ClearRefinements.tsx b/packages/instantsearch.js/src/components/ClearRefinements/ClearRefinements.tsx index aa776d002f4..637ecf2e440 100644 --- a/packages/instantsearch.js/src/components/ClearRefinements/ClearRefinements.tsx +++ b/packages/instantsearch.js/src/components/ClearRefinements/ClearRefinements.tsx @@ -5,7 +5,7 @@ import { h } from 'preact'; import Template from '../Template/Template'; -import type { ClearRefinementsRenderState } from '../../connectors/clear-refinements/connectClearRefinements'; +import type { ClearRefinementsRenderState } from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { ComponentCSSClasses } from '../../types'; import type { diff --git a/packages/instantsearch.js/src/components/CurrentRefinements/CurrentRefinements.tsx b/packages/instantsearch.js/src/components/CurrentRefinements/CurrentRefinements.tsx index d2d9d48e0be..a78b652adf5 100644 --- a/packages/instantsearch.js/src/components/CurrentRefinements/CurrentRefinements.tsx +++ b/packages/instantsearch.js/src/components/CurrentRefinements/CurrentRefinements.tsx @@ -8,7 +8,7 @@ import { isSpecialClick, capitalize } from '../../lib/utils'; import type { CurrentRefinementsConnectorParamsItem, CurrentRefinementsConnectorParamsRefinement, -} from '../../connectors/current-refinements/connectCurrentRefinements'; +} from '../../connectors'; import type { ComponentCSSClasses } from '../../types'; import type { CurrentRefinementsCSSClasses } from '../../widgets/current-refinements/current-refinements'; diff --git a/packages/instantsearch.js/src/components/MenuSelect/MenuSelect.tsx b/packages/instantsearch.js/src/components/MenuSelect/MenuSelect.tsx index 1b4e39e8afe..41b376f26a1 100644 --- a/packages/instantsearch.js/src/components/MenuSelect/MenuSelect.tsx +++ b/packages/instantsearch.js/src/components/MenuSelect/MenuSelect.tsx @@ -6,7 +6,7 @@ import { h } from 'preact'; import { find } from '../../lib/utils'; import Template from '../Template/Template'; -import type { MenuRenderState } from '../../connectors/menu/connectMenu'; +import type { MenuRenderState } from '../../connectors'; import type { ComponentCSSClasses } from '../../types'; import type { MenuSelectCSSClasses, diff --git a/packages/instantsearch.js/src/components/Panel/__tests__/Panel-test.tsx b/packages/instantsearch.js/src/components/Panel/__tests__/Panel-test.tsx index 18214b3cdb8..eb9c8161257 100644 --- a/packages/instantsearch.js/src/components/Panel/__tests__/Panel-test.tsx +++ b/packages/instantsearch.js/src/components/Panel/__tests__/Panel-test.tsx @@ -6,7 +6,7 @@ import { render, fireEvent } from '@testing-library/preact'; import { h } from 'preact'; -import { createRenderOptions } from '../../../../test/createWidget'; +import { createRenderOptions } from '../../../../../instantsearch-core/test/createWidget'; import Panel from '../Panel'; import type { PanelProps } from '../Panel'; diff --git a/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx b/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx index c6786e57560..c7ca4debc7c 100644 --- a/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx +++ b/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx @@ -8,7 +8,7 @@ import Template from '../Template/Template'; import type { Range, RangeBoundaries, -} from '../../connectors/range/connectRange'; +} from '../../connectors'; import type { ComponentCSSClasses } from '../../types'; import type { RangeInputCSSClasses, diff --git a/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx b/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx index 942bd6634cb..9bd2b039e79 100644 --- a/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx +++ b/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx @@ -9,7 +9,7 @@ import Template from '../Template/Template'; import RefinementListItem from './RefinementListItem'; -import type { HierarchicalMenuItem } from '../../connectors/hierarchical-menu/connectHierarchicalMenu'; +import type { HierarchicalMenuItem } from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { ComponentCSSClasses, CreateURL, Templates } from '../../types'; import type { HierarchicalMenuComponentCSSClasses } from '../../widgets/hierarchical-menu/hierarchical-menu'; diff --git a/packages/instantsearch.js/src/components/Slider/Slider.tsx b/packages/instantsearch.js/src/components/Slider/Slider.tsx index 3ef245780c6..008ff82c0d2 100644 --- a/packages/instantsearch.js/src/components/Slider/Slider.tsx +++ b/packages/instantsearch.js/src/components/Slider/Slider.tsx @@ -8,7 +8,7 @@ import { range } from '../../lib/utils'; import Pit from './Pit'; import Rheostat from './Rheostat'; -import type { RangeBoundaries } from '../../connectors/range/connectRange'; +import type { RangeBoundaries } from '../../connectors'; import type { ComponentCSSClasses } from '../../types'; import type { RangeSliderCssClasses, diff --git a/packages/instantsearch.js/src/components/ToggleRefinement/ToggleRefinement.tsx b/packages/instantsearch.js/src/components/ToggleRefinement/ToggleRefinement.tsx index bc57db747d8..67ffa3d7033 100644 --- a/packages/instantsearch.js/src/components/ToggleRefinement/ToggleRefinement.tsx +++ b/packages/instantsearch.js/src/components/ToggleRefinement/ToggleRefinement.tsx @@ -7,7 +7,7 @@ import Template from '../Template/Template'; import type { ToggleRefinementRenderState, ToggleRefinementValue, -} from '../../connectors/toggle-refinement/connectToggleRefinement'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { ComponentCSSClasses } from '../../types'; import type { diff --git a/packages/instantsearch.js/src/components/VoiceSearch/VoiceSearch.tsx b/packages/instantsearch.js/src/components/VoiceSearch/VoiceSearch.tsx index 02508d69d02..246099ac844 100644 --- a/packages/instantsearch.js/src/components/VoiceSearch/VoiceSearch.tsx +++ b/packages/instantsearch.js/src/components/VoiceSearch/VoiceSearch.tsx @@ -4,12 +4,12 @@ import { h } from 'preact'; import Template from '../Template/Template'; -import type { VoiceListeningState } from '../../lib/voiceSearchHelper/types'; import type { ComponentCSSClasses } from '../../types'; import type { VoiceSearchCSSClasses, VoiceSearchTemplates, } from '../../widgets/voice-search/voice-search'; +import type { VoiceListeningState } from 'instantsearch-core'; export type VoiceSearchComponentCSSClasses = ComponentCSSClasses; diff --git a/packages/instantsearch.js/src/connectors/answers/__tests__/connectAnswers-test.ts b/packages/instantsearch.js/src/connectors/answers/__tests__/connectAnswers-test.ts index d3e33b6e326..e27f85daa15 100644 --- a/packages/instantsearch.js/src/connectors/answers/__tests__/connectAnswers-test.ts +++ b/packages/instantsearch.js/src/connectors/answers/__tests__/connectAnswers-test.ts @@ -9,11 +9,11 @@ import { import { wait } from '@instantsearch/testutils/wait'; import algoliasearchHelper, { SearchResults } from 'algoliasearch-helper'; -import { createInstantSearch } from '../../../../test/createInstantSearch'; +import { createInstantSearch } from '../../../../../instantsearch-core/test/createInstantSearch'; import { createInitOptions, createRenderOptions, -} from '../../../../test/createWidget'; +} from '../../../../../instantsearch-core/test/createWidget'; import connectAnswers from '../connectAnswers'; const defaultRenderDebounceTime = 10; diff --git a/packages/instantsearch.js/src/connectors/configure-related-items/__tests__/connectConfigureRelatedItems-test-bridge.ts b/packages/instantsearch.js/src/connectors/configure-related-items/__tests__/connectConfigureRelatedItems-test-bridge.ts index 8e0c0af87d0..63f0ac65fc6 100644 --- a/packages/instantsearch.js/src/connectors/configure-related-items/__tests__/connectConfigureRelatedItems-test-bridge.ts +++ b/packages/instantsearch.js/src/connectors/configure-related-items/__tests__/connectConfigureRelatedItems-test-bridge.ts @@ -1,9 +1,12 @@ import { castToJestMock } from '@instantsearch/testutils/castToJestMock'; +import { connectConfigure } from 'instantsearch-core'; -import connectConfigure from '../../configure/connectConfigure'; import connectConfigureRelatedItems from '../connectConfigureRelatedItems'; -jest.mock('../../configure/connectConfigure'); +jest.mock('instantsearch-core', () => ({ + ...jest.requireActual('instantsearch-core'), + connectConfigure: jest.fn(), +})); // This test ensures that the `connectConfigureRelatedItems` calls // `connectConfigure` to compute the search parameters. diff --git a/packages/instantsearch.js/src/connectors/configure-related-items/connectConfigureRelatedItems.ts b/packages/instantsearch.js/src/connectors/configure-related-items/connectConfigureRelatedItems.ts index e4bb2ac9585..c4d7c341b74 100644 --- a/packages/instantsearch.js/src/connectors/configure-related-items/connectConfigureRelatedItems.ts +++ b/packages/instantsearch.js/src/connectors/configure-related-items/connectConfigureRelatedItems.ts @@ -1,4 +1,5 @@ import algoliasearchHelper from 'algoliasearch-helper'; +import { connectConfigure } from 'instantsearch-core'; import { createDocumentationMessageGenerator, @@ -6,10 +7,12 @@ import { warning, getPropertyByPath, } from '../../lib/utils'; -import connectConfigure from '../configure/connectConfigure'; -import type { AlgoliaHit, Connector } from '../../types'; -import type { ConfigureWidgetDescription } from '../configure/connectConfigure'; +import type { + AlgoliaHit, + Connector, + ConfigureWidgetDescription, +} from '../../types'; import type { SearchParameters, PlainSearchParameters, diff --git a/packages/instantsearch.js/src/connectors/feeds/FeedContainer.ts b/packages/instantsearch.js/src/connectors/feeds/FeedContainer.ts index 828ef23e69d..f63d274b31c 100644 --- a/packages/instantsearch.js/src/connectors/feeds/FeedContainer.ts +++ b/packages/instantsearch.js/src/connectors/feeds/FeedContainer.ts @@ -1,310 +1,2 @@ -import algoliasearchHelper from 'algoliasearch-helper'; - -import { - createInitArgs, - createRenderArgs, - storeRenderState, -} from '../../lib/utils'; - -import type { - InstantSearch, - UiState, - IndexUiState, - Widget, - IndexWidget, - DisposeOptions, - RenderOptions, -} from '../../types'; -import type { SearchParameters } from 'algoliasearch-helper'; - -export function createFeedContainer( - feedID: string, - parentIndex: IndexWidget, - instantSearchInstance: InstantSearch -): IndexWidget { - let localWidgets: Array = []; - let initialized = false; - - const container: IndexWidget = { - $$type: 'ais.feedContainer', - $$widgetType: 'ais.feedContainer', - _isolated: true, - - getIndexName: () => parentIndex.getIndexName(), - getIndexId: () => feedID, - getHelper: () => parentIndex.getHelper(), - - getResults() { - const parentResults = parentIndex.getResults(); - if (!parentResults) return null; - if (!parentResults.feeds) { - // Single-feed backward compat: no feeds array means the parent result - // itself is the only feed. - if (feedID === '') { - parentResults._state = parentIndex.getHelper()!.state; - return parentResults; - } - return null; - } - const feed = parentResults.feeds.find((f) => f.feedID === feedID); - if (!feed) return null; - // Optimistic state patching — same as index widget (index.ts:365-370) - feed._state = parentIndex.getHelper()!.state; - return feed; - }, - - getResultsForWidget() { - return this.getResults(); - }, - - getParent: () => parentIndex, - getWidgets: () => localWidgets, - getScopedResults: () => parentIndex.getScopedResults(), - getPreviousState: () => null, - createURL: ( - nextState: SearchParameters | ((state: IndexUiState) => IndexUiState) - ) => parentIndex.createURL(nextState), - scheduleLocalSearch: () => parentIndex.scheduleLocalSearch(), - - addWidgets(widgets) { - const flatWidgets = widgets.reduce>( - (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), - [] - ); - flatWidgets.forEach((widget) => { - widget.parent = container; - }); - localWidgets = localWidgets.concat(flatWidgets); - - if (initialized) { - flatWidgets.forEach((widget) => { - if (widget.getRenderState) { - const renderState = widget.getRenderState( - instantSearchInstance.renderState[container.getIndexId()] || {}, - createInitArgs( - instantSearchInstance, - container, - instantSearchInstance._initialUiState - ) - ); - storeRenderState({ - renderState, - instantSearchInstance, - parent: container, - }); - } - }); - - flatWidgets.forEach((widget) => { - if (widget.init) { - widget.init( - createInitArgs( - instantSearchInstance, - container, - instantSearchInstance._initialUiState - ) - ); - } - }); - - // Merge children's search params (e.g. disjunctiveFacets) into the - // parent's helper state so they're included in the composition request. - // uiState is {} because URL-derived refinements are already on the - // parent state; children only need to declare structural params. - const parentHelper = parentIndex.getHelper()!; - const withChildParams = container.getWidgetSearchParameters( - parentHelper.state, - { uiState: {} } - ); - if (withChildParams !== parentHelper.state) { - parentHelper.setState(withChildParams); - } - } - - return container; - }, - - removeWidgets(widgets) { - const flatWidgets = widgets.reduce>( - (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), - [] - ); - const helper = parentIndex.getHelper(); - - if (!helper) { - localWidgets = localWidgets.filter((w) => !flatWidgets.includes(w)); - return container; - } - - // Chain through children's dispose so widgets clean up the - // SearchParameters they declared (e.g. RefinementList removes its - // disjunctiveFacet) instead of leaving them stale on the parent helper. - let cleanedState: SearchParameters = helper.state; - - flatWidgets.forEach((widget) => { - if (widget.dispose) { - const next = widget.dispose({ - helper, - state: cleanedState, - recommendState: helper.recommendState, - parent: container, - }); - - if (next instanceof algoliasearchHelper.RecommendParameters) { - // ignore — FeedContainer doesn't manage recommend state - } else if (next) { - cleanedState = next; - } - } - }); - - localWidgets = localWidgets.filter((w) => !flatWidgets.includes(w)); - - if (cleanedState !== helper.state) { - helper.setState(cleanedState); - } - - return container; - }, - - init() { - initialized = true; - - localWidgets.forEach((widget) => { - if (widget.getRenderState) { - const renderState = widget.getRenderState( - instantSearchInstance.renderState[container.getIndexId()] || {}, - createInitArgs( - instantSearchInstance, - container, - instantSearchInstance._initialUiState - ) - ); - storeRenderState({ - renderState, - instantSearchInstance, - parent: container, - }); - } - }); - - localWidgets.forEach((widget) => { - if (widget.init) { - widget.init( - createInitArgs( - instantSearchInstance, - container, - instantSearchInstance._initialUiState - ) - ); - } - }); - }, - - render() { - localWidgets.forEach((widget) => { - if (widget.getRenderState) { - const renderState = widget.getRenderState( - instantSearchInstance.renderState[container.getIndexId()] || {}, - createRenderArgs( - instantSearchInstance, - container, - widget - ) as RenderOptions - ); - storeRenderState({ - renderState, - instantSearchInstance, - parent: container, - }); - } - }); - - localWidgets.forEach((widget) => { - if (widget.render) { - widget.render( - createRenderArgs( - instantSearchInstance, - container, - widget - ) as RenderOptions - ); - } - }); - }, - - dispose(disposeOptions?: DisposeOptions) { - const helper = parentIndex.getHelper(); - - // Chain through children's dispose to return a cleaned state - // (e.g. RefinementList.dispose removes its disjunctiveFacet declaration). - // This mirrors how the index widget's removeWidgets chains dispose calls. - let cleanedState = disposeOptions?.state ?? helper?.state; - - localWidgets.forEach((widget) => { - if (widget.dispose && helper) { - const next = widget.dispose({ - helper, - state: cleanedState!, - recommendState: helper.recommendState, - parent: container, - }); - - if (next instanceof algoliasearchHelper.RecommendParameters) { - // ignore — FeedContainer doesn't manage recommend state - } else if (next) { - cleanedState = next; - } - } - }); - - localWidgets = []; - initialized = false; - return cleanedState; - }, - - getWidgetState(uiState: UiState) { - return this.getWidgetUiState(uiState); - }, - - getWidgetUiState( - uiState: TUiState - ): TUiState { - const helper = parentIndex.getHelper()!; - const widgetUiStateOptions = { - searchParameters: helper.state, - helper, - }; - return localWidgets.reduce( - (state, widget) => - widget.getWidgetUiState - ? (widget.getWidgetUiState(state, widgetUiStateOptions) as TUiState) - : state, - uiState - ); - }, - - getWidgetSearchParameters( - searchParameters: SearchParameters, - { uiState }: { uiState: IndexUiState } - ) { - return localWidgets.reduce( - (params, widget) => - widget.getWidgetSearchParameters - ? widget.getWidgetSearchParameters(params, { uiState }) - : params, - searchParameters - ); - }, - - refreshUiState() { - // no-op: FeedContainer doesn't own UI state - }, - - setIndexUiState() { - // no-op: FeedContainer delegates to parent - }, - }; - - return container; -} +export { createFeedContainer } from 'instantsearch-core'; +export type * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/connectors/index.ts b/packages/instantsearch.js/src/connectors/index.ts index d43db5fb5ab..f16f96504f9 100644 --- a/packages/instantsearch.js/src/connectors/index.ts +++ b/packages/instantsearch.js/src/connectors/index.ts @@ -1,8 +1,9 @@ +import { connectDynamicWidgets } from 'instantsearch-core'; + import { deprecate } from '../lib/utils'; import connectAnswers from './answers/connectAnswers'; import connectConfigureRelatedItems from './configure-related-items/connectConfigureRelatedItems'; -import connectDynamicWidgets from './dynamic-widgets/connectDynamicWidgets'; /** @deprecated answers is no longer supported */ export const EXPERIMENTAL_connectAnswers = deprecate( @@ -22,40 +23,251 @@ export const EXPERIMENTAL_connectDynamicWidgets = deprecate( 'use connectDynamicWidgets' ); -export { connectDynamicWidgets }; - -export { default as connectClearRefinements } from './clear-refinements/connectClearRefinements'; -export { default as connectCurrentRefinements } from './current-refinements/connectCurrentRefinements'; -export { default as connectHierarchicalMenu } from './hierarchical-menu/connectHierarchicalMenu'; -export { default as connectHits } from './hits/connectHits'; -export { default as connectHitsWithInsights } from './hits/connectHitsWithInsights'; -export { default as connectHitsPerPage } from './hits-per-page/connectHitsPerPage'; -export { default as connectInfiniteHits } from './infinite-hits/connectInfiniteHits'; -export { default as connectInfiniteHitsWithInsights } from './infinite-hits/connectInfiniteHitsWithInsights'; -export { default as connectMenu } from './menu/connectMenu'; -export { default as connectNumericMenu } from './numeric-menu/connectNumericMenu'; -export { default as connectPagination } from './pagination/connectPagination'; -export { default as connectRange } from './range/connectRange'; -export { default as connectRefinementList } from './refinement-list/connectRefinementList'; -export { default as connectRelatedProducts } from './related-products/connectRelatedProducts'; -export { default as connectSearchBox } from './search-box/connectSearchBox'; -export { default as connectSortBy } from './sort-by/connectSortBy'; -export { default as connectRatingMenu } from './rating-menu/connectRatingMenu'; -export { default as connectStats } from './stats/connectStats'; -export { default as connectToggleRefinement } from './toggle-refinement/connectToggleRefinement'; -export { default as connectTrendingFacets } from './trending-facets/connectTrendingFacets'; -export { default as connectTrendingItems } from './trending-items/connectTrendingItems'; -export { default as connectBreadcrumb } from './breadcrumb/connectBreadcrumb'; -export { default as connectGeoSearch } from './geo-search/connectGeoSearch'; -export { default as connectPoweredBy } from './powered-by/connectPoweredBy'; -export { default as connectConfigure } from './configure/connectConfigure'; -export { default as connectAutocomplete } from './autocomplete/connectAutocomplete'; -export { default as connectQueryRules } from './query-rules/connectQueryRules'; -export { default as connectVoiceSearch } from './voice-search/connectVoiceSearch'; -export { default as connectRelevantSort } from './relevant-sort/connectRelevantSort'; -export { default as connectFrequentlyBoughtTogether } from './frequently-bought-together/connectFrequentlyBoughtTogether'; -export { default as connectLookingSimilar } from './looking-similar/connectLookingSimilar'; -export { default as connectChat } from './chat/connectChat'; -export { default as connectFeeds } from './feeds/connectFeeds'; -export { default as connectChatTrigger } from './chat/connectChatTrigger'; -export { default as connectFilterSuggestions } from './filter-suggestions/connectFilterSuggestions'; +export { connectAutocomplete } from 'instantsearch-core'; +export { connectBreadcrumb } from 'instantsearch-core'; +export { connectChat } from 'instantsearch-core'; +export { connectChatTrigger } from 'instantsearch-core'; +export { connectClearRefinements } from 'instantsearch-core'; +export { connectConfigure } from 'instantsearch-core'; +export { connectCurrentRefinements } from 'instantsearch-core'; +export { connectDynamicWidgets } from 'instantsearch-core'; +export { connectFeeds } from 'instantsearch-core'; +export { connectFilterSuggestions } from 'instantsearch-core'; +export { connectFrequentlyBoughtTogether } from 'instantsearch-core'; +export { connectGeoSearch } from 'instantsearch-core'; +export { connectHierarchicalMenu } from 'instantsearch-core'; +export { connectHits } from 'instantsearch-core'; +export { connectHitsWithInsights } from 'instantsearch-core'; +export { connectHitsPerPage } from 'instantsearch-core'; +export { connectInfiniteHits } from 'instantsearch-core'; +export { connectInfiniteHitsWithInsights } from 'instantsearch-core'; +export { connectLookingSimilar } from 'instantsearch-core'; +export { connectMenu } from 'instantsearch-core'; +export { connectNumericMenu } from 'instantsearch-core'; +export { connectPagination } from 'instantsearch-core'; +export { connectPoweredBy } from 'instantsearch-core'; +export { connectQueryRules } from 'instantsearch-core'; +export { connectRange } from 'instantsearch-core'; +export { connectRatingMenu } from 'instantsearch-core'; +export { connectRefinementList } from 'instantsearch-core'; +export { connectRelatedProducts } from 'instantsearch-core'; +export { connectRelevantSort } from 'instantsearch-core'; +export { connectSearchBox } from 'instantsearch-core'; +export { connectSortBy } from 'instantsearch-core'; +export { connectStats } from 'instantsearch-core'; +export { connectToggleRefinement } from 'instantsearch-core'; +export { connectTrendingFacets } from 'instantsearch-core'; +export { connectTrendingItems } from 'instantsearch-core'; +export { connectVoiceSearch } from 'instantsearch-core'; +export { createFeedContainer } from 'instantsearch-core'; +export type { + ApplyFiltersParams, + AutocompleteConnector, + AutocompleteConnectorParams, + AutocompleteRenderState, + AutocompleteWidgetDescription, + BreadcrumbConnector, + BreadcrumbConnectorParams, + BreadcrumbConnectorParamsItem +} from 'instantsearch-core'; +export type { + BreadcrumbRenderState, + BreadcrumbWidgetDescription, + ChatConnector, + ChatConnectorParams, + ChatInit, + ChatInitWithoutTransport, + ChatRenderState, + ChatTransport +} from 'instantsearch-core'; +export type { + ChatTriggerConnector, + ChatTriggerConnectorParams, + ChatTriggerRenderState, + ChatTriggerWidgetDescription +} from 'instantsearch-core'; +export type { + ChatWidgetDescription, + ClearRefinementsConnector, + ClearRefinementsConnectorParams, + ClearRefinementsRenderState, + ClearRefinementsWidgetDescription, + ConfigureConnector, + ConfigureConnectorParams, + ConfigureRenderState +} from 'instantsearch-core'; +export type { + ConfigureWidgetDescription, + CurrentRefinementsConnector, + CurrentRefinementsConnectorParams, + CurrentRefinementsConnectorParamsItem, + CurrentRefinementsConnectorParamsRefinement, + CurrentRefinementsRenderState, + CurrentRefinementsWidgetDescription, + DynamicWidgetsConnector +} from 'instantsearch-core'; +export type { + DynamicWidgetsConnectorParams, + DynamicWidgetsRenderState, + DynamicWidgetsWidgetDescription, + FeedsConnector, + FeedsConnectorParams, + FeedsRenderState, + FeedsWidgetDescription, + FilterSuggestionsConnector +} from 'instantsearch-core'; +export type { + FilterSuggestionsConnectorParams, + FilterSuggestionsRenderState, + FilterSuggestionsTransport, + FilterSuggestionsWidgetDescription, + FrequentlyBoughtTogetherConnector, + FrequentlyBoughtTogetherConnectorParams, + FrequentlyBoughtTogetherRenderState, + FrequentlyBoughtTogetherWidgetDescription +} from 'instantsearch-core'; +export type { + GeoHit, + GeoSearchConnector, + GeoSearchConnectorParams, + GeoSearchRenderState, + GeoSearchWidgetDescription, + HierarchicalMenuConnector, + HierarchicalMenuConnectorParams, + HierarchicalMenuItem +} from 'instantsearch-core'; +export type { + HierarchicalMenuRenderState, + HierarchicalMenuWidgetDescription, + HitsConnector, + HitsConnectorParams, + HitsPerPageConnector, + HitsPerPageConnectorParams, + HitsPerPageConnectorParamsItem, + HitsPerPageRenderState +} from 'instantsearch-core'; +export type { + HitsPerPageRenderStateItem, + HitsPerPageWidgetDescription, + HitsRenderState, + HitsWidgetDescription, + InfiniteHitsCache, + InfiniteHitsCachedHits, + InfiniteHitsConnector, + InfiniteHitsConnectorParams +} from 'instantsearch-core'; +export type { + InfiniteHitsRenderState, + InfiniteHitsWidgetDescription, + LookingSimilarConnector, + LookingSimilarConnectorParams, + LookingSimilarRenderState, + LookingSimilarWidgetDescription, + MenuConnector, + MenuConnectorParams +} from 'instantsearch-core'; +export type { + MenuItem, + MenuRenderState, + MenuWidgetDescription, + NumericMenuConnector, + NumericMenuConnectorParams, + NumericMenuConnectorParamsItem, + NumericMenuRenderState, + NumericMenuRenderStateItem +} from 'instantsearch-core'; +export type { + NumericMenuWidgetDescription, + PaginationConnector, + PaginationConnectorParams, + PaginationRenderState, + PaginationWidgetDescription, + ParamTrackedFilters, + ParamTransformRuleContexts, + PoweredByConnector +} from 'instantsearch-core'; +export type { + PoweredByConnectorParams, + PoweredByRenderState, + PoweredByWidgetDescription, + QueryRulesConnector, + QueryRulesConnectorParams, + QueryRulesRenderState, + QueryRulesWidgetDescription, + Range +} from 'instantsearch-core'; +export type { + RangeBoundaries, + RangeConnector, + RangeConnectorParams, + RangeMax, + RangeMin, + RangeRenderState, + RangeWidgetDescription, + RatingMenuConnector +} from 'instantsearch-core'; +export type { + RatingMenuConnectorParams, + RatingMenuRenderState, + RatingMenuWidgetDescription, + RefinementListConnector, + RefinementListConnectorParams, + RefinementListItem, + RefinementListRenderState, + RefinementListWidgetDescription +} from 'instantsearch-core'; +export type { + RelatedProductsConnector, + RelatedProductsConnectorParams, + RelatedProductsRenderState, + RelatedProductsWidgetDescription, + RelevantSortConnector, + RelevantSortConnectorParams, + RelevantSortRenderState, + RelevantSortWidgetDescription +} from 'instantsearch-core'; +export type { + SearchBoxConnector, + SearchBoxConnectorParams, + SearchBoxRenderState, + SearchBoxWidgetDescription, + SendEventForToggle, + SortByConnector, + SortByConnectorParams, + SortByIndexItem +} from 'instantsearch-core'; +export type { + SortByItem, + SortByRenderState, + SortByStrategyItem, + SortByWidgetDescription, + StatsConnector, + StatsConnectorParams, + StatsRenderState, + StatsWidgetDescription +} from 'instantsearch-core'; +export type { + Suggestion, + ToggleRefinementConnector, + ToggleRefinementConnectorParams, + ToggleRefinementRenderState, + ToggleRefinementValue, + ToggleRefinementWidgetDescription, + TransformItemsIndicesConfig, + TrendingFacetsConnector +} from 'instantsearch-core'; +export type { + TrendingFacetsConnectorParams, + TrendingFacetsRenderState, + TrendingFacetsWidgetDescription, + TrendingItemsConnector, + TrendingItemsConnectorParams, + TrendingItemsRenderState, + TrendingItemsWidgetDescription, + VoiceSearchConnector +} from 'instantsearch-core'; +export type { + VoiceSearchConnectorParams, + VoiceSearchRenderState, + VoiceSearchWidgetDescription +} from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/connectors/toggle-refinement/types.ts b/packages/instantsearch.js/src/connectors/toggle-refinement/types.ts index 5ebbd8461bd..d06d350019a 100644 --- a/packages/instantsearch.js/src/connectors/toggle-refinement/types.ts +++ b/packages/instantsearch.js/src/connectors/toggle-refinement/types.ts @@ -1,6 +1,6 @@ export type { - /** @deprecated import from connectToggleRefinement directly */ + /** @deprecated import from instantsearch-core directly */ ToggleRefinementConnector, - /** @deprecated import from connectToggleRefinement directly */ + /** @deprecated import from instantsearch-core directly */ ToggleRefinementWidgetDescription, -} from './connectToggleRefinement'; +} from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/InstantSearch.ts b/packages/instantsearch.js/src/lib/InstantSearch.ts index 19baddd252a..1bdd74f690e 100644 --- a/packages/instantsearch.js/src/lib/InstantSearch.ts +++ b/packages/instantsearch.js/src/lib/InstantSearch.ts @@ -1,901 +1,40 @@ -import EventEmitter from '@algolia/events'; -import algoliasearchHelper from 'algoliasearch-helper'; - -import { createInsightsMiddleware } from '../middlewares/createInsightsMiddleware'; import { - createMetadataMiddleware, - isMetadataEnabled, -} from '../middlewares/createMetadataMiddleware'; -import { createRouterMiddleware } from '../middlewares/createRouterMiddleware'; -import index from '../widgets/index/index'; + InstantSearch as CoreInstantSearch, + INSTANTSEARCH_FUTURE_DEFAULTS, +} from 'instantsearch-core'; import createHelpers from './createHelpers'; -import { - createDocumentationMessageGenerator, - createDocumentationLink, - defer, - hydrateRecommendCache, - hydrateSearchClient, - noop, - warning, - setIndexHelperState, - isIndexWidget, -} from './utils'; import version from './version'; import type { - InsightsEvent, - InsightsProps, -} from '../middlewares/createInsightsMiddleware'; -import type { RouterProps } from '../middlewares/createRouterMiddleware'; -import type { - InsightsClient as AlgoliaInsightsClient, - SearchClient, - Widget, - IndexWidget, + InstantSearchOptions, + InstantSearchStatus, UiState, - CreateURL, - Middleware, - MiddlewareDefinition, - RenderState, - InitialResults, - CompositionClient, -} from '../types'; -import type { AlgoliaSearchHelper } from 'algoliasearch-helper'; - -const withUsage = createDocumentationMessageGenerator({ - name: 'instantsearch', -}); - -function defaultCreateURL() { - return '#'; -} - -// this purposely breaks typescript's type inference to ensure it's not used -// as it's used for a default parameter for example -// source: https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-504042546 -type NoInfer = T extends infer S ? S : never; - -/** - * Global options for an InstantSearch instance. - */ -export type InstantSearchOptions< - TUiState extends UiState = UiState, - TRouteState = TUiState -> = { - /** - * The name of the main index. If no indexName is provided, you have to manually add an index widget. - */ - indexName?: string; - - /** - * The objectID of the composition. - * If this is passed, the composition API will be used for search. - * Multi-index search is not supported with this option. - */ - compositionID?: string; - - /** - * The search client to plug to InstantSearch.js - * - * Usage: - * ```javascript - * // Using the default Algolia search client - * instantsearch({ - * indexName: 'indexName', - * searchClient: algoliasearch('appId', 'apiKey') - * }); - * - * // Using a custom search client - * instantsearch({ - * indexName: 'indexName', - * searchClient: { - * search(requests) { - * // fetch response based on requests - * return response; - * }, - * searchForFacetValues(requests) { - * // fetch response based on requests - * return response; - * } - * } - * }); - * ``` - */ - searchClient: SearchClient | CompositionClient; - - /** - * The locale used to display numbers. This will be passed - * to `Number.prototype.toLocaleString()` - */ - numberLocale?: string; - - /** - * A hook that will be called each time a search needs to be done, with the - * helper as a parameter. It's your responsibility to call `helper.search()`. - * This option allows you to avoid doing searches at page load for example. - * @deprecated use onStateChange instead - */ - searchFunction?: (helper: AlgoliaSearchHelper) => void; - - /** - * Function called when the state changes. - * - * Using this function makes the instance controlled. This means that you - * become in charge of updating the UI state with the `setUiState` function. - */ - onStateChange?: (params: { - uiState: TUiState; - setUiState: ( - uiState: TUiState | ((previousUiState: TUiState) => TUiState) - ) => void; - }) => void; - - /** - * Injects a `uiState` to the `instantsearch` instance. You can use this option - * to provide an initial state to a widget. Note that the state is only used - * for the first search. To unconditionally pass additional parameters to the - * Algolia API, take a look at the `configure` widget. - */ - initialUiState?: NoInfer; - - /** - * Time before a search is considered stalled. The default is 200ms - */ - stalledSearchDelay?: number; - - /** - * Router configuration used to save the UI State into the URL or any other - * client side persistence. Passing `true` will use the default URL options. - */ - routing?: RouterProps | boolean; - - /** - * Enables the Insights middleware and loads the Insights library - * if not already loaded. - * - * The Insights middleware sends view and click events automatically, and lets - * you set up your own events. - * - * @default false - */ - insights?: InsightsProps | boolean; - - /** - * the instance of search-insights to use for sending insights events inside - * widgets like `hits`. - * - * @deprecated This property will be still supported in 4.x releases, but not further. It is replaced by the `insights` middleware. For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/ - */ - insightsClient?: AlgoliaInsightsClient; - future?: { - /** - * Changes the way `dispose` is used in InstantSearch lifecycle. - * - * If `false` (by default), each widget unmounting will remove its state as well, even if there are multiple widgets reading that UI State. - * - * If `true`, each widget unmounting will only remove its own state if it's the last of its type. This allows for dynamically adding and removing widgets without losing their state. - * - * @default false - */ - // @MAJOR: Remove legacy behaviour - preserveSharedStateOnUnmount?: boolean; - /** - * Changes the way root levels of hierarchical facets have their count displayed. - * - * If `false` (by default), the count of the refined root level is updated to match the count of the actively refined parent level. - * - * If `true`, the count of the root level stays the same as the count of all children levels. - * - * @default false - */ - // @MAJOR: Remove legacy behaviour here and in algoliasearch-helper - persistHierarchicalRootCount?: boolean; - }; -}; - -export type InstantSearchStatus = 'idle' | 'loading' | 'stalled' | 'error'; +} from 'instantsearch-core'; -export const INSTANTSEARCH_FUTURE_DEFAULTS: Required< - InstantSearchOptions['future'] -> = { - preserveSharedStateOnUnmount: false, - persistHierarchicalRootCount: false, -}; +export { INSTANTSEARCH_FUTURE_DEFAULTS }; +export type { InstantSearchOptions, InstantSearchStatus }; -/** - * The actual implementation of the InstantSearch. This is - * created using the `instantsearch` factory function. - * It emits the 'render' event every time a search is done - */ class InstantSearch< TUiState extends UiState = UiState, TRouteState = TUiState -> extends EventEmitter { - public client: InstantSearchOptions['searchClient']; - public indexName: string; - public compositionID?: string; - public insightsClient: AlgoliaInsightsClient | null; - public onStateChange: InstantSearchOptions['onStateChange'] | null = - null; - public future: NonNullable['future']>; - public helper: AlgoliaSearchHelper | null; - public mainHelper: AlgoliaSearchHelper | null; - public mainIndex: IndexWidget; - public started: boolean; - public templatesConfig: Record; - public renderState: RenderState = {}; - public _stalledSearchDelay: number; - public _searchStalledTimer: any; - public _initialUiState: TUiState; - public _initialResults: InitialResults | null; - public _manuallyResetScheduleSearch: boolean = false; - public _resetScheduleSearch?: () => void; - public _createURL: CreateURL; - public _searchFunction?: InstantSearchOptions['searchFunction']; - public _mainHelperSearch?: AlgoliaSearchHelper['search']; - public _hasSearchWidget: boolean = false; - public _hasRecommendWidget: boolean = false; - public _insights: InstantSearchOptions['insights']; - public middleware: Array<{ - creator: Middleware; - instance: MiddlewareDefinition; - }> = []; - public sendEventToInsights: (event: InsightsEvent) => void; - /** - * The status of the search. Can be "idle", "loading", "stalled", or "error". - */ - public status: InstantSearchStatus = 'idle'; - /** - * The last returned error from the Search API. - * The error gets cleared when the next valid search response is rendered. - */ - public error: Error | undefined = undefined; - - /** - * @deprecated use `status === 'stalled'` instead - */ - public get _isSearchStalled(): boolean { - warning( - false, - `\`InstantSearch._isSearchStalled\` is deprecated and will be removed in InstantSearch.js 5.0. - -Use \`InstantSearch.status === "stalled"\` instead.` - ); - - return this.status === 'stalled'; - } - - public constructor(options: InstantSearchOptions) { - super(); - - // prevent `render` event listening from causing a warning - this.setMaxListeners(100); - - const { - indexName = '', - compositionID, - numberLocale, - initialUiState = {} as TUiState, - routing = null, - insights = undefined, - searchFunction, - stalledSearchDelay = 200, - searchClient = null, - insightsClient = null, - onStateChange = null, - future = { - ...INSTANTSEARCH_FUTURE_DEFAULTS, - ...(options.future || {}), - }, - } = options; - - if (searchClient === null) { - throw new Error(withUsage('The `searchClient` option is required.')); - } - - if (typeof searchClient.search !== 'function') { - throw new Error( - `The \`searchClient\` must implement a \`search\` method. - -See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/` - ); - } - - if (typeof searchClient.addAlgoliaAgent === 'function') { - searchClient.addAlgoliaAgent(`instantsearch.js (${version})`); - } - - warning( - insightsClient === null, - `\`insightsClient\` property has been deprecated. It is still supported in 4.x releases, but not further. It is replaced by the \`insights\` middleware. - -For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/` - ); - - if (insightsClient && typeof insightsClient !== 'function') { - throw new Error( - withUsage('The `insightsClient` option should be a function.') - ); - } - - warning( - !(options as any).searchParameters, - `The \`searchParameters\` option is deprecated and will not be supported in InstantSearch.js 4.x. - -You can replace it with the \`configure\` widget: - -\`\`\` -search.addWidgets([ - configure(${JSON.stringify((options as any).searchParameters, null, 2)}) -]); -\`\`\` - -See ${createDocumentationLink({ - name: 'configure', - })}` - ); - - if (__DEV__ && options.future?.preserveSharedStateOnUnmount === undefined) { - // eslint-disable-next-line no-console - console.info(`Starting from the next major version, InstantSearch will change how widgets state is preserved when they are removed. InstantSearch will keep the state of unmounted widgets to be usable by other widgets with the same attribute. - -We recommend setting \`future.preserveSharedStateOnUnmount\` to true to adopt this change today. -To stay with the current behaviour and remove this warning, set the option to false. - -See documentation: ${createDocumentationLink({ - name: 'instantsearch', - })}#widget-param-future - `); - } - - this.client = searchClient; - this.future = future; - this.insightsClient = insightsClient; - this.indexName = indexName; - this.compositionID = compositionID; - this.helper = null; - this.mainHelper = null; - this.mainIndex = index({ - // we use an index widget to render compositions - // this only works because there's only one composition index allow for now - indexName: this.compositionID || this.indexName, +> extends CoreInstantSearch { + constructor(options: InstantSearchOptions) { + super({ + ...options, + _internalHelpers: createHelpers({ numberLocale: options.numberLocale }), }); - this.onStateChange = onStateChange; - - this.started = false; - this.templatesConfig = { - helpers: createHelpers({ numberLocale }), - compileOptions: {}, - }; - - this._stalledSearchDelay = stalledSearchDelay; - this._searchStalledTimer = null; - - this._createURL = defaultCreateURL; - this._initialUiState = initialUiState as TUiState; - this._initialResults = null; - - this._insights = insights; - - if (searchFunction) { - warning( - false, - `The \`searchFunction\` option is deprecated. Use \`onStateChange\` instead.` - ); - this._searchFunction = searchFunction; - } - - this.sendEventToInsights = noop; - - if (routing) { - const routerOptions = typeof routing === 'boolean' ? {} : routing; - routerOptions.$$internal = true; - this.use(createRouterMiddleware(routerOptions)); - } - - // This is the default Insights middleware, - // added when `insights` is set to true by the user. - // Any user-provided middleware will be added later and override this one. - if (insights) { - const insightsOptions = typeof insights === 'boolean' ? {} : insights; - insightsOptions.$$internal = true; - this.use(createInsightsMiddleware(insightsOptions)); - } - - if (isMetadataEnabled()) { - this.use(createMetadataMiddleware({ $$internal: true })); - } - } - - /** - * Hooks a middleware into the InstantSearch lifecycle. - */ - public use(...middleware: Array>): this { - const newMiddlewareList = middleware.map((fn) => { - const newMiddleware = { - $$type: '__unknown__', - $$internal: false, - subscribe: noop, - started: noop, - unsubscribe: noop, - onStateChange: noop, - ...fn({ - instantSearchInstance: this as unknown as InstantSearch< - UiState, - UiState - >, - }), - }; - this.middleware.push({ - creator: fn, - instance: newMiddleware, - }); - return newMiddleware; - }); - - // If the instance has already started, we directly subscribe the - // middleware so they're notified of changes. - if (this.started) { - newMiddlewareList.forEach((m) => { - m.subscribe(); - m.started(); - }); - } - - return this; - } - - /** - * Removes a middleware from the InstantSearch lifecycle. - */ - public unuse(...middlewareToUnuse: Array>): this { - this.middleware - .filter((m) => middlewareToUnuse.includes(m.creator)) - .forEach((m) => m.instance.unsubscribe()); - - this.middleware = this.middleware.filter( - (m) => !middlewareToUnuse.includes(m.creator) - ); - - return this; - } - - // @major we shipped with EXPERIMENTAL_use, but have changed that to just `use` now - public EXPERIMENTAL_use(...middleware: Middleware[]): this { - warning( - false, - 'The middleware API is now considered stable, so we recommend replacing `EXPERIMENTAL_use` with `use` before upgrading to the next major version.' - ); - - return this.use(...middleware); - } - - /** - * Adds a widget to the search instance. - * A widget can be added either before or after InstantSearch has started. - * @param widget The widget to add to InstantSearch. - * - * @deprecated This method will still be supported in 4.x releases, but not further. It is replaced by `addWidgets([widget])`. - */ - public addWidget(widget: Widget) { - warning( - false, - 'addWidget will still be supported in 4.x releases, but not further. It is replaced by `addWidgets([widget])`' - ); - - return this.addWidgets([widget]); - } - - /** - * Adds multiple widgets to the search instance. - * Widgets can be added either before or after InstantSearch has started. - * @param widgets The array of widgets to add to InstantSearch. - */ - public addWidgets( - widgets: Array> - ) { - if (!Array.isArray(widgets)) { - throw new Error( - withUsage( - 'The `addWidgets` method expects an array of widgets. Please use `addWidget`.' - ) - ); - } + const { searchClient } = options; if ( - this.compositionID && - widgets.some((w) => !Array.isArray(w) && isIndexWidget(w) && !w._isolated) + searchClient && + typeof (searchClient as { addAlgoliaAgent?: unknown }) + .addAlgoliaAgent === 'function' ) { - throw new Error( - withUsage( - 'The `index` widget cannot be used with a composition-based InstantSearch implementation.' - ) - ); + ( + searchClient as { addAlgoliaAgent: (agent: string) => void } + ).addAlgoliaAgent(`instantsearch.js (${version})`); } - - this.mainIndex.addWidgets(widgets); - - return this; - } - - /** - * Removes a widget from the search instance. - * @deprecated This method will still be supported in 4.x releases, but not further. It is replaced by `removeWidgets([widget])` - * @param widget The widget instance to remove from InstantSearch. - * - * The widget must implement a `dispose()` method to clear its state. - */ - public removeWidget(widget: Widget | IndexWidget) { - warning( - false, - 'removeWidget will still be supported in 4.x releases, but not further. It is replaced by `removeWidgets([widget])`' - ); - - return this.removeWidgets([widget]); - } - - /** - * Removes multiple widgets from the search instance. - * @param widgets Array of widgets instances to remove from InstantSearch. - * - * The widgets must implement a `dispose()` method to clear their states. - */ - public removeWidgets(widgets: Array) { - if (!Array.isArray(widgets)) { - throw new Error( - withUsage( - 'The `removeWidgets` method expects an array of widgets. Please use `removeWidget`.' - ) - ); - } - - this.mainIndex.removeWidgets(widgets); - - return this; - } - - /** - * Ends the initialization of InstantSearch.js and triggers the - * first search. - */ - public start() { - if (this.started) { - throw new Error( - withUsage('The `start` method has already been called once.') - ); - } - - // This Helper is used for the queries, we don't care about its state. The - // states are managed at the `index` level. We use this Helper to create - // DerivedHelper scoped into the `index` widgets. - // In Vue InstantSearch' hydrate, a main helper gets set before start, so - // we need to respect this helper as a way to keep all listeners correct. - const mainHelper = - this.mainHelper || - algoliasearchHelper(this.client, this.indexName, undefined, { - persistHierarchicalRootCount: this.future.persistHierarchicalRootCount, - }); - - if (this.compositionID) { - mainHelper.searchForFacetValues = - mainHelper.searchForCompositionFacetValues.bind(mainHelper); - } - - mainHelper.search = () => { - this.status = 'loading'; - this.scheduleRender(false); - - warning( - Boolean(this.indexName) || - Boolean(this.compositionID) || - this.mainIndex.getWidgets().some(isIndexWidget), - 'No indexName provided, nor an explicit index widget in the widgets tree. This is required to be able to display results.' - ); - - // This solution allows us to keep the exact same API for the users but - // under the hood, we have a different implementation. It should be - // completely transparent for the rest of the codebase. Only this module - // is impacted. - if (this._hasSearchWidget) { - if (this.compositionID) { - mainHelper.searchWithComposition(); - } else { - mainHelper.searchOnlyWithDerivedHelpers(); - } - } - - if (this._hasRecommendWidget) { - mainHelper.recommend(); - } - - return mainHelper; - }; - - if (this._searchFunction) { - // this client isn't used to actually search, but required for the helper - // to not throw errors - const fakeClient = { - search: () => new Promise(noop), - } as any as SearchClient; - - this._mainHelperSearch = mainHelper.search.bind(mainHelper); - mainHelper.search = () => { - const mainIndexHelper = this.mainIndex.getHelper(); - const searchFunctionHelper = algoliasearchHelper( - fakeClient, - mainIndexHelper!.state.index, - mainIndexHelper!.state - ); - searchFunctionHelper.once('search', ({ state }) => { - mainIndexHelper!.overrideStateWithoutTriggeringChangeEvent(state); - this._mainHelperSearch!(); - }); - // Forward state changes from `searchFunctionHelper` to `mainIndexHelper` - searchFunctionHelper.on('change', ({ state }) => { - mainIndexHelper!.setState(state); - }); - this._searchFunction!(searchFunctionHelper); - return mainHelper; - }; - } - - // Only the "main" Helper emits the `error` event vs the one for `search` - // and `results` that are also emitted on the derived one. - mainHelper.on('error', ({ error }) => { - if (!(error instanceof Error)) { - // typescript lies here, error is in some cases { name: string, message: string } - const err = error as Record; - error = Object.keys(err).reduce((acc, key) => { - (acc as any)[key] = err[key]; - return acc; - }, new Error(err.message)); - } - // If an error is emitted, it is re-thrown by events. In previous versions - // we emitted {error}, which is thrown as: - // "Uncaught, unspecified \"error\" event. ([object Object])" - // To avoid breaking changes, we make the error available in both - // `error` and `error.error` - // @MAJOR emit only error - (error as any).error = error; - this.error = error; - this.status = 'error'; - this.scheduleRender(false); - - // This needs to execute last because it throws the error. - this.emit('error', error); - }); - - this.mainHelper = mainHelper; - - this.middleware.forEach(({ instance }) => { - instance.subscribe(); - }); - - this.mainIndex.init({ - instantSearchInstance: this as unknown as InstantSearch, - parent: null, - uiState: this._initialUiState, - }); - - if (this._initialResults) { - hydrateSearchClient(this.client, this._initialResults); - hydrateRecommendCache(this.mainHelper, this._initialResults); - - const originalScheduleSearch = this.scheduleSearch; - // We don't schedule a first search when initial results are provided - // because we already have the results to render. This skips the initial - // network request on the browser on `start`. - this.scheduleSearch = defer(noop); - if (this._manuallyResetScheduleSearch) { - // If `_manuallyResetScheduleSearch` is passed, it means that we don't - // want to rely on a single `defer` to reset the `scheduleSearch`. - // Instead, the consumer will call `_resetScheduleSearch` to restore - // the original `scheduleSearch` function. - // This happens in the React flavour after rendering. - this._resetScheduleSearch = () => { - this.scheduleSearch = originalScheduleSearch; - }; - } else { - // We also skip the initial network request when widgets are dynamically - // added in the first tick (that's the case in all the framework-based flavors). - // When we add a widget to `index`, it calls `scheduleSearch`. We can rely - // on our `defer` util to restore the original `scheduleSearch` value once - // widgets are added to hook back to the regular lifecycle. - defer(() => { - this.scheduleSearch = originalScheduleSearch; - })(); - } - } - // We only schedule a search when widgets have been added before `start()` - // because there are listeners that can use these results. - // This is especially useful in framework-based flavors that wait for - // dynamically-added widgets to trigger a network request. It avoids - // having to batch this initial network request with the one coming from - // `addWidgets()`. - // Later, we could also skip `index()` widgets and widgets that don't read - // the results, but this is an optimization that has a very low impact for now. - else if (this.mainIndex.getWidgets().length > 0) { - this.scheduleSearch(); - } - - // Keep the previous reference for legacy purpose, some pattern use - // the direct Helper access `search.helper` (e.g multi-index). - this.helper = this.mainIndex.getHelper(); - - // track we started the search if we add more widgets, - // to init them directly after add - this.started = true; - - this.middleware.forEach(({ instance }) => { - instance.started(); - }); - - // This is the automatic Insights middleware, - // added when `insights` is unset and the initial results possess `queryID`. - // Any user-provided middleware will be added later and override this one. - if (typeof this._insights === 'undefined') { - mainHelper.derivedHelpers[0].once('result', () => { - const hasAutomaticInsights = this.mainIndex - .getScopedResults() - .some(({ results }) => results?._automaticInsights); - if (hasAutomaticInsights) { - this.use( - createInsightsMiddleware({ - $$internal: true, - $$automatic: true, - }) - ); - } - }); - } - } - - /** - * Removes all widgets without triggering a search afterwards. - * @return {undefined} This method does not return anything - */ - public dispose(): void { - this.scheduleSearch.cancel(); - this.scheduleRender.cancel(); - clearTimeout(this._searchStalledTimer); - - this.removeWidgets(this.mainIndex.getWidgets()); - this.mainIndex.dispose(); - - // You can not start an instance two times, therefore a disposed instance - // needs to set started as false otherwise this can not be restarted at a - // later point. - this.started = false; - - // The helper needs to be reset to perform the next search from a fresh state. - // If not reset, it would use the state stored before calling `dispose()`. - this.removeAllListeners(); - this.mainHelper?.removeAllListeners(); - this.mainHelper = null; - this.helper = null; - - this.middleware.forEach(({ instance }) => { - instance.unsubscribe(); - }); - } - - public scheduleSearch = defer(() => { - if (this.started) { - this.mainHelper!.search(); - } - }); - - public scheduleRender = defer((shouldResetStatus: boolean = true) => { - if (!this.mainHelper?.hasPendingRequests()) { - clearTimeout(this._searchStalledTimer); - this._searchStalledTimer = null; - - if (shouldResetStatus) { - this.status = 'idle'; - this.error = undefined; - } - } - - this.mainIndex.render({ - instantSearchInstance: this as unknown as InstantSearch, - }); - - this.emit('render'); - }); - - public scheduleStalledRender() { - if (!this._searchStalledTimer) { - this._searchStalledTimer = setTimeout(() => { - this.status = 'stalled'; - this.scheduleRender(); - }, this._stalledSearchDelay); - } - } - - /** - * Set the UI state and trigger a search. - * @param uiState The next UI state or a function computing it from the current state - * @param callOnStateChange private parameter used to know if the method is called from a state change - */ - public setUiState( - uiState: TUiState | ((previousUiState: TUiState) => TUiState), - callOnStateChange: boolean = true - ): void { - if (!this.mainHelper) { - throw new Error( - withUsage('The `start` method needs to be called before `setUiState`.') - ); - } - - // We refresh the index UI state to update the local UI state that the - // main index passes to the function form of `setUiState`. - this.mainIndex.refreshUiState(); - const nextUiState = - typeof uiState === 'function' - ? uiState(this.mainIndex.getWidgetUiState({}) as TUiState) - : uiState; - - if (this.onStateChange && callOnStateChange) { - this.onStateChange({ - uiState: nextUiState, - setUiState: (finalUiState) => { - setIndexHelperState( - typeof finalUiState === 'function' - ? finalUiState(nextUiState) - : finalUiState, - this.mainIndex - ); - - this.scheduleSearch(); - this.onInternalStateChange(); - }, - }); - } else { - setIndexHelperState(nextUiState, this.mainIndex); - - this.scheduleSearch(); - this.onInternalStateChange(); - } - } - - public getUiState(): TUiState { - if (this.started) { - // We refresh the index UI state to make sure changes from `refine` are taken in account - this.mainIndex.refreshUiState(); - } - - return this.mainIndex.getWidgetUiState({}) as TUiState; - } - - public onInternalStateChange = defer(() => { - const nextUiState = this.mainIndex.getWidgetUiState({}) as TUiState; - - this.middleware.forEach(({ instance }) => { - instance.onStateChange({ - uiState: nextUiState, - }); - }); - }); - - public createURL(nextState: TUiState = {} as TUiState): string { - if (!this.started) { - throw new Error( - withUsage('The `start` method needs to be called before `createURL`.') - ); - } - - return this._createURL(nextState); - } - - public refresh() { - if (!this.mainHelper) { - throw new Error( - withUsage('The `start` method needs to be called before `refresh`.') - ); - } - - this.mainHelper.clearCache().search(); } } diff --git a/packages/instantsearch.js/src/lib/__tests__/InstantSearch-agent-test.ts b/packages/instantsearch.js/src/lib/__tests__/InstantSearch-agent-test.ts new file mode 100644 index 00000000000..16afbb5d0b4 --- /dev/null +++ b/packages/instantsearch.js/src/lib/__tests__/InstantSearch-agent-test.ts @@ -0,0 +1,26 @@ +/** + * @jest-environment @instantsearch/testutils/jest-environment-jsdom.ts + */ + +import { createSearchClient } from '@instantsearch/mocks'; + +import InstantSearch from '../InstantSearch'; +import version from '../version'; + +describe('InstantSearch (IS.js subclass)', () => { + it('adds the `instantsearch.js` user agent', () => { + const searchClient = createSearchClient({ + addAlgoliaAgent: jest.fn(), + }); + + // eslint-disable-next-line no-new + new InstantSearch({ + indexName: 'indexName', + searchClient, + }); + + expect(searchClient.addAlgoliaAgent).toHaveBeenCalledWith( + `instantsearch.js (${version})` + ); + }); +}); diff --git a/packages/instantsearch.js/src/lib/__tests__/RoutingManager-test.ts b/packages/instantsearch.js/src/lib/__tests__/RoutingManager-test.ts index e006f49b83b..ad02afdc73b 100644 --- a/packages/instantsearch.js/src/lib/__tests__/RoutingManager-test.ts +++ b/packages/instantsearch.js/src/lib/__tests__/RoutingManager-test.ts @@ -7,7 +7,7 @@ import { wait } from '@instantsearch/testutils/wait'; import qs from 'qs'; import instantsearch from '../..'; -import { createWidget } from '../../../test/createWidget'; +import { createWidget } from '../../../../instantsearch-core/test/createWidget'; import { connectHitsPerPage, connectSearchBox } from '../../connectors'; import historyRouter from '../routers/history'; diff --git a/packages/instantsearch.js/src/lib/__tests__/composition-multifeed-ssr.test.ts b/packages/instantsearch.js/src/lib/__tests__/composition-multifeed-ssr.test.ts index a0944df78ab..c6e4f13859e 100644 --- a/packages/instantsearch.js/src/lib/__tests__/composition-multifeed-ssr.test.ts +++ b/packages/instantsearch.js/src/lib/__tests__/composition-multifeed-ssr.test.ts @@ -6,11 +6,11 @@ import { createCompositionClient, createSingleSearchResponse, } from '@instantsearch/mocks'; +import { hydrateSearchClient } from 'instantsearch-core'; import { connectFeeds, connectSearchBox } from '../../connectors'; import instantsearch from '../../index.es'; import { getInitialResults, waitForResults } from '../server'; -import { hydrateSearchClient } from '../utils/hydrateSearchClient'; const compositionID = 'my-comp'; diff --git a/packages/instantsearch.js/src/lib/infiniteHitsCache/index.ts b/packages/instantsearch.js/src/lib/infiniteHitsCache/index.ts index 6fd47d4f820..1b4fd8425dc 100644 --- a/packages/instantsearch.js/src/lib/infiniteHitsCache/index.ts +++ b/packages/instantsearch.js/src/lib/infiniteHitsCache/index.ts @@ -1 +1 @@ -export { default as createInfiniteHitsSessionStorageCache } from './sessionStorage'; +export * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/infiniteHitsCache/sessionStorage.ts b/packages/instantsearch.js/src/lib/infiniteHitsCache/sessionStorage.ts index af4d74d6fbd..b8b955c2596 100644 --- a/packages/instantsearch.js/src/lib/infiniteHitsCache/sessionStorage.ts +++ b/packages/instantsearch.js/src/lib/infiniteHitsCache/sessionStorage.ts @@ -1,74 +1,2 @@ -import { isEqual, safelyRunOnBrowser } from '../utils'; - -import type { InfiniteHitsCache } from '../../connectors/infinite-hits/connectInfiniteHits'; -import type { PlainSearchParameters } from 'algoliasearch-helper'; - -function getStateWithoutPage(state: PlainSearchParameters) { - const { page, ...rest } = state || {}; - return rest; -} - -export default function createInfiniteHitsSessionStorageCache({ - key, -}: { - /** - * If you display multiple instances of infiniteHits on the same page, - * you must provide a unique key for each instance. - */ - key?: string; -} = {}): InfiniteHitsCache { - const KEY = ['ais.infiniteHits', key].filter(Boolean).join(':'); - - return { - read({ state }) { - const sessionStorage = safelyRunOnBrowser( - ({ window }) => window.sessionStorage - ); - - if (!sessionStorage) { - return null; - } - - try { - const cache = JSON.parse( - // @ts-expect-error JSON.parse() requires a string, but it actually accepts null, too. - sessionStorage.getItem(KEY) - ); - - return cache && isEqual(cache.state, getStateWithoutPage(state)) - ? cache.hits - : null; - } catch (error) { - if (error instanceof SyntaxError) { - try { - sessionStorage.removeItem(KEY); - } catch (err) { - // do nothing - } - } - return null; - } - }, - write({ state, hits }) { - const sessionStorage = safelyRunOnBrowser( - ({ window }) => window.sessionStorage - ); - - if (!sessionStorage) { - return; - } - - try { - sessionStorage.setItem( - KEY, - JSON.stringify({ - state: getStateWithoutPage(state), - hits, - }) - ); - } catch (error) { - // do nothing - } - }, - }; -} +export { createInfiniteHitsSessionStorageCache as default } from 'instantsearch-core'; +export type * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts b/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts index bddce6db683..17f2e3ab646 100644 --- a/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts +++ b/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts @@ -2,7 +2,7 @@ import { createSingleSearchResponse } from '@instantsearch/mocks'; import { SearchParameters, SearchResults } from 'algoliasearch-helper'; import { withInsights, inferInsightsPayload } from '../'; -import { createInstantSearch } from '../../../../test/createInstantSearch'; +import { createInstantSearch } from '../../../../../instantsearch-core/test/createInstantSearch'; import type { InstantSearch, Widget } from '../../../types'; diff --git a/packages/instantsearch.js/src/lib/insights/__tests__/insights-listener-test.tsx b/packages/instantsearch.js/src/lib/insights/__tests__/insights-listener-test.tsx index 5113e5f49f9..6fe4fcf58d3 100644 --- a/packages/instantsearch.js/src/lib/insights/__tests__/insights-listener-test.tsx +++ b/packages/instantsearch.js/src/lib/insights/__tests__/insights-listener-test.tsx @@ -8,7 +8,7 @@ import { render, fireEvent } from '@testing-library/preact'; import algoliasearchHelper from 'algoliasearch-helper'; import { h } from 'preact'; -import { createInstantSearch } from '../../../../test/createInstantSearch'; +import { createInstantSearch } from '../../../../../instantsearch-core/test/createInstantSearch'; import { _buildEventPayloadsForHits, serializePayload, diff --git a/packages/instantsearch.js/src/lib/insights/listener.tsx b/packages/instantsearch.js/src/lib/insights/listener.tsx index 6c479926046..999098e6285 100644 --- a/packages/instantsearch.js/src/lib/insights/listener.tsx +++ b/packages/instantsearch.js/src/lib/insights/listener.tsx @@ -5,7 +5,7 @@ import { h } from 'preact'; import { readDataAttributes } from '../../helpers/insights'; import { deserializePayload, warning } from '../utils'; -import type { InsightsEvent } from '../../middlewares/createInsightsMiddleware'; +import type { InsightsEvent } from '../../middlewares'; import type { InsightsClient } from '../../types'; export type InsightsEventHandlerOptions = { diff --git a/packages/instantsearch.js/src/lib/routers/history.ts b/packages/instantsearch.js/src/lib/routers/history.ts index 4c7f34baefa..2660a4cf6ea 100644 --- a/packages/instantsearch.js/src/lib/routers/history.ts +++ b/packages/instantsearch.js/src/lib/routers/history.ts @@ -1,371 +1,2 @@ -import qs from 'qs'; - -import { createDocumentationLink, safelyRunOnBrowser, warning } from '../utils'; - -import type { Router, UiState } from '../../types'; - -type CreateURL = (args: { - qsModule: typeof qs; - routeState: TRouteState; - location: Location; -}) => string; - -type ParseURL = (args: { - qsModule: typeof qs; - location: Location; -}) => TRouteState; - -export type BrowserHistoryArgs = { - windowTitle?: (routeState: TRouteState) => string; - writeDelay: number; - createURL: CreateURL; - parseURL: ParseURL; - // @MAJOR: The `Location` type is hard to simulate in non-browser environments - // so we should accept a subset of it that is easier to work with in any - // environments. - getLocation: () => Location; - start?: (onUpdate: () => void) => void; - dispose?: () => void; - push?: (url: string) => void; - /** - * Whether the URL should be cleaned up when the router is disposed. - * This can be useful when closing a modal containing InstantSearch, to - * remove active refinements from the URL. - * @default true - */ - // @MAJOR: Switch the default to `false` and remove the console info in the next major version. - cleanUrlOnDispose?: boolean; -}; - -const setWindowTitle = (title?: string): void => { - if (title) { - // This function is only executed on browsers so we can disable this check. - // eslint-disable-next-line no-restricted-globals - window.document.title = title; - } -}; - -class BrowserHistory implements Router { - public $$type = 'ais.browser'; - /** - * Transforms a UI state into a title for the page. - */ - private readonly windowTitle?: BrowserHistoryArgs['windowTitle']; - /** - * Time in milliseconds before performing a write in the history. - * It prevents from adding too many entries in the history and - * makes the back button more usable. - * - * @default 400 - */ - private readonly writeDelay: Required< - BrowserHistoryArgs - >['writeDelay']; - /** - * Creates a full URL based on the route state. - * The storage adaptor maps all syncable keys to the query string of the URL. - */ - private readonly _createURL: Required< - BrowserHistoryArgs - >['createURL']; - /** - * Parses the URL into a route state. - * It should be symmetrical to `createURL`. - */ - private readonly parseURL: Required< - BrowserHistoryArgs - >['parseURL']; - /** - * Returns the location to store in the history. - * @default () => window.location - */ - private readonly getLocation: Required< - BrowserHistoryArgs - >['getLocation']; - - private writeTimer?: ReturnType; - private _onPopState?: (event: PopStateEvent) => void; - - /** - * Indicates if last action was back/forward in the browser. - */ - private inPopState: boolean = false; - - /** - * Indicates whether the history router is disposed or not. - */ - protected isDisposed: boolean = false; - - /** - * Indicates the window.history.length before the last call to - * window.history.pushState (called in `write`). - * It allows to determine if a `pushState` has been triggered elsewhere, - * and thus to prevent the `write` method from calling `pushState`. - */ - private latestAcknowledgedHistory: number = 0; - - private _start?: (onUpdate: () => void) => void; - private _dispose?: () => void; - private _push?: (url: string) => void; - private _cleanUrlOnDispose: boolean; - - /** - * Initializes a new storage provider that syncs the search state to the URL - * using web APIs (`window.location.pushState` and `onpopstate` event). - */ - public constructor({ - windowTitle, - writeDelay = 400, - createURL, - parseURL, - getLocation, - start, - dispose, - push, - cleanUrlOnDispose, - }: BrowserHistoryArgs) { - this.windowTitle = windowTitle; - this.writeTimer = undefined; - this.writeDelay = writeDelay; - this._createURL = createURL; - this.parseURL = parseURL; - this.getLocation = getLocation; - this._start = start; - this._dispose = dispose; - this._push = push; - this._cleanUrlOnDispose = - typeof cleanUrlOnDispose === 'undefined' ? true : cleanUrlOnDispose; - - if (__DEV__ && typeof cleanUrlOnDispose === 'undefined') { - // eslint-disable-next-line no-console - console.info(`Starting from the next major version, InstantSearch will not clean up the URL from active refinements when it is disposed. - -We recommend setting \`cleanUrlOnDispose\` to false to adopt this change today. -To stay with the current behaviour and remove this warning, set the option to true. - -See documentation: ${createDocumentationLink({ - name: 'history-router', - })}#widget-param-cleanurlondispose`); - } - - safelyRunOnBrowser(({ window: browserWindow }) => { - const title = this.windowTitle && this.windowTitle(this.read()); - setWindowTitle(title); - - this.latestAcknowledgedHistory = browserWindow.history.length; - }); - } - - /** - * Reads the URL and returns a syncable UI search state. - */ - public read(): TRouteState { - return this.parseURL({ qsModule: qs, location: this.getLocation() }); - } - - /** - * Pushes a search state into the URL. - */ - public write(routeState: TRouteState): void { - safelyRunOnBrowser(({ window: browserWindow }) => { - const url = this.createURL(routeState); - const title = this.windowTitle && this.windowTitle(routeState); - - if (this.writeTimer) { - clearTimeout(this.writeTimer); - } - - this.writeTimer = setTimeout(() => { - setWindowTitle(title); - - if (this.shouldWrite(url)) { - if (this._push) { - this._push(url); - } else { - browserWindow.history.pushState(routeState, title || '', url); - } - this.latestAcknowledgedHistory = browserWindow.history.length; - } - this.inPopState = false; - this.writeTimer = undefined; - }, this.writeDelay); - }); - } - - /** - * Sets a callback on the `onpopstate` event of the history API of the current page. - * It enables the URL sync to keep track of the changes. - */ - public onUpdate(callback: (routeState: TRouteState) => void): void { - if (this._start) { - this._start(() => { - callback(this.read()); - }); - } - - this._onPopState = () => { - if (this.writeTimer) { - clearTimeout(this.writeTimer); - this.writeTimer = undefined; - } - - this.inPopState = true; - - // We always read the state from the URL because the state of the history - // can be incorect in some cases (e.g. using React Router). - callback(this.read()); - }; - - safelyRunOnBrowser(({ window: browserWindow }) => { - browserWindow.addEventListener('popstate', this._onPopState!); - }); - } - - /** - * Creates a complete URL from a given syncable UI state. - * - * It always generates the full URL, not a relative one. - * This allows to handle cases like using a . - * See: https://github.com/algolia/instantsearch/issues/790 - */ - public createURL(routeState: TRouteState): string { - const url = this._createURL({ - qsModule: qs, - routeState, - location: this.getLocation(), - }); - - if (__DEV__) { - try { - // We just want to check if the URL is valid. - // eslint-disable-next-line no-new - new URL(url); - } catch (e) { - warning( - false, - `The URL returned by the \`createURL\` function is invalid. -Please make sure it returns an absolute URL to avoid issues, e.g: \`https://algolia.com/search?query=iphone\`.` - ); - } - } - - return url; - } - - /** - * Removes the event listener and cleans up the URL. - */ - public dispose(): void { - if (this._dispose) { - this._dispose(); - } - - this.isDisposed = true; - - safelyRunOnBrowser(({ window: browserWindow }) => { - if (this._onPopState) { - browserWindow.removeEventListener('popstate', this._onPopState); - } - }); - - if (this.writeTimer) { - clearTimeout(this.writeTimer); - } - - if (this._cleanUrlOnDispose) { - this.write({} as TRouteState); - } - } - - public start() { - this.isDisposed = false; - } - - private shouldWrite(url: string): boolean { - return safelyRunOnBrowser(({ window: browserWindow }) => { - // When disposed and the cleanUrlOnDispose is set to false, we do not want to write the URL. - if (this.isDisposed && !this._cleanUrlOnDispose) { - return false; - } - - // We do want to `pushState` if: - // - the router is not disposed, IS.js needs to update the URL - // OR - // - the last write was from InstantSearch.js - // (unlike a SPA, where it would have last written) - const lastPushWasByISAfterDispose = !( - this.isDisposed && - this.latestAcknowledgedHistory !== browserWindow.history.length - ); - - return ( - // When the last state change was through popstate, the IS.js state changes, - // but that should not write the URL. - !this.inPopState && - // When the previous pushState after dispose was by IS.js, we want to write the URL. - lastPushWasByISAfterDispose && - // When the URL is the same as the current one, we do not want to write it. - url !== browserWindow.location.href - ); - }); - } -} - -export default function historyRouter({ - createURL = ({ qsModule, routeState, location }) => { - const { protocol, hostname, port = '', pathname, hash } = location; - const queryString = qsModule.stringify(routeState); - const portWithPrefix = port === '' ? '' : `:${port}`; - - // IE <= 11 has no proper `location.origin` so we cannot rely on it. - if (!queryString) { - return `${protocol}//${hostname}${portWithPrefix}${pathname}${hash}`; - } - - return `${protocol}//${hostname}${portWithPrefix}${pathname}?${queryString}${hash}`; - }, - parseURL = ({ qsModule, location }) => { - // `qs` by default converts arrays with more than 20 items to an object. - // We want to avoid this because the data structure manipulated can therefore vary. - // Setting the limit to `100` seems a good number because the engine's default is 100 - // (it can go up to 1000 but it is very unlikely to select more than 100 items in the UI). - // - // Using an `arrayLimit` of `n` allows `n + 1` items. - // - // See: - // - https://github.com/ljharb/qs#parsing-arrays - // - https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/ - return qsModule.parse(location.search.slice(1), { - arrayLimit: 99, - }) as unknown as TRouteState; - }, - writeDelay = 400, - windowTitle, - getLocation = () => { - return safelyRunOnBrowser( - ({ window: browserWindow }) => browserWindow.location, - { - fallback: () => { - throw new Error( - 'You need to provide `getLocation` to the `history` router in environments where `window` does not exist.' - ); - }, - }); - }, - start, - dispose, - push, - cleanUrlOnDispose, -}: Partial> = {}): BrowserHistory { - return new BrowserHistory({ - createURL, - parseURL, - writeDelay, - windowTitle, - getLocation, - start, - dispose, - push, - cleanUrlOnDispose, - }); -} +export { history as default } from 'instantsearch-core'; +export type * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/routers/index.ts b/packages/instantsearch.js/src/lib/routers/index.ts index 5742d94c8bc..3a16f3cada6 100644 --- a/packages/instantsearch.js/src/lib/routers/index.ts +++ b/packages/instantsearch.js/src/lib/routers/index.ts @@ -1 +1 @@ -export { default as history } from './history'; +export { history } from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/server.ts b/packages/instantsearch.js/src/lib/server.ts index 99cfce33fc4..1b4fd8425dc 100644 --- a/packages/instantsearch.js/src/lib/server.ts +++ b/packages/instantsearch.js/src/lib/server.ts @@ -1,150 +1 @@ -import { walkIndex } from './utils'; - -import type { - SearchClient, - CompositionClient, - IndexWidget, - InitialResults, - InstantSearch, - SearchOptions, -} from '../types'; - -/** - * Waits for the results from the search instance to coordinate the next steps - * in `getServerState()`. - */ -export function waitForResults( - search: InstantSearch, - skipRecommend: boolean = false -): Promise { - const helper = search.mainHelper!; - - // Extract search parameters from the search client to use them - // later during hydration. - let requestParamsList: SearchOptions[]; - const client = helper.getClient(); - if (search.compositionID) { - helper.setClient({ - ...client, - search(query) { - requestParamsList = [query.requestBody.params]; - return (client as CompositionClient).search(query); - }, - } as CompositionClient); - } else { - helper.setClient({ - ...client, - search(queries) { - requestParamsList = queries.map(({ params }) => params); - return (client as SearchClient).search(queries); - }, - } as SearchClient); - } - - if (search._hasSearchWidget) { - if (search.compositionID) { - helper.searchWithComposition(); - } else { - helper.searchOnlyWithDerivedHelpers(); - } - } - !skipRecommend && search._hasRecommendWidget && helper.recommend(); - - return new Promise((resolve, reject) => { - let searchResultsReceived = !search._hasSearchWidget; - let recommendResultsReceived = !search._hasRecommendWidget || skipRecommend; - // All derived helpers resolve in the same tick so we're safe only relying - // on the first one. - helper.derivedHelpers[0].on('result', () => { - searchResultsReceived = true; - if (recommendResultsReceived) { - resolve(requestParamsList!); - } - }); - helper.derivedHelpers[0].on('recommend:result', () => { - recommendResultsReceived = true; - if (searchResultsReceived) { - resolve(requestParamsList!); - } - }); - - // However, we listen to errors that can happen on any derived helper because - // any error is critical. - helper.on('error', (error) => { - reject(error); - }); - search.on('error', (error) => { - reject(error); - }); - helper.derivedHelpers.forEach((derivedHelper) => - derivedHelper.on('error', (error) => { - reject(error); - }) - ); - }); -} - -/** - * Walks the InstantSearch root index to construct the initial results. - */ -export function getInitialResults( - rootIndex: IndexWidget, - /** - * Search parameters sent to the search client, - * returned by `waitForResults()`. - */ - requestParamsList?: SearchOptions[] -): InitialResults { - const initialResults: InitialResults = {}; - - let requestParamsIndex = 0; - walkIndex(rootIndex, (widget) => { - const searchResults = widget.getResults(); - const recommendResults = widget.getHelper()?.lastRecommendResults; - if (searchResults || recommendResults) { - const resultsCount = searchResults?._rawResults?.length || 0; - const requestParams = resultsCount - ? requestParamsList?.slice( - requestParamsIndex, - requestParamsIndex + resultsCount - ) - : []; - requestParamsIndex += resultsCount; - initialResults[widget.getIndexId()] = { - // We convert the Helper state to a plain object to pass parsable data - // structures from server to client. - ...(searchResults && { - state: { - ...searchResults._state, - clickAnalytics: requestParams?.[0]?.clickAnalytics, - userToken: requestParams?.[0]?.userToken, - }, - results: searchResults._rawResults, - ...(searchResults.feeds && - searchResults.feeds.length > 0 && { - compositionFeedsResults: searchResults.feeds.map((feed) => ({ - ...feed._rawResults[0], - feedID: feed.feedID, - })), - }), - }), - ...(recommendResults && { - recommendResults: { - // We have to stringify + parse because of some explicitly undefined values. - params: JSON.parse(JSON.stringify(recommendResults._state.params)), - results: recommendResults._rawResults, - }, - }), - ...(requestParams && { requestParams }), - }; - } - }); - - if (Object.keys(initialResults).length === 0) { - throw new Error( - 'The root index does not have any results. Make sure you have at least one widget that provides results.' - ); - } - - return initialResults; -} +export * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/stateMappings/index.ts b/packages/instantsearch.js/src/lib/stateMappings/index.ts index 8691b834268..5919ba7e7e7 100644 --- a/packages/instantsearch.js/src/lib/stateMappings/index.ts +++ b/packages/instantsearch.js/src/lib/stateMappings/index.ts @@ -1,2 +1 @@ -export { default as simple } from './simple'; -export { default as singleIndex } from './singleIndex'; +export { simple, singleIndex } from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/stateMappings/simple.ts b/packages/instantsearch.js/src/lib/stateMappings/simple.ts index 3ddc3a21a3f..c3254d8cc06 100644 --- a/packages/instantsearch.js/src/lib/stateMappings/simple.ts +++ b/packages/instantsearch.js/src/lib/stateMappings/simple.ts @@ -1,45 +1,2 @@ -import type { UiState, IndexUiState, StateMapping } from '../../types'; - -function getIndexStateWithoutConfigure( - uiState: TIndexUiState -): Omit { - const { configure, ...trackedUiState } = uiState; - return trackedUiState; -} - -// technically a URL could contain any key, since users provide it, -// which is why the input to this function is UiState, not something -// which excludes "configure" as this function does. -export default function simpleStateMapping< - TUiState extends UiState = UiState ->(): StateMapping { - return { - $$type: 'ais.simple', - - stateToRoute(uiState) { - return Object.keys(uiState).reduce( - (state, indexId) => ({ - ...state, - [indexId]: getIndexStateWithoutConfigure(uiState[indexId]), - }), - {} as TUiState - ); - }, - - routeToState(routeState = {} as TUiState) { - return Object.keys(routeState).reduce( - (state, indexId) => { - const indexState = routeState[indexId]; - if (typeof indexState !== 'object' || indexState === null) { - return state; - } - return { - ...state, - [indexId]: getIndexStateWithoutConfigure(indexState), - }; - }, - {} as TUiState - ); - }, - }; -} +export { simple as default } from 'instantsearch-core'; +export type * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/stateMappings/singleIndex.ts b/packages/instantsearch.js/src/lib/stateMappings/singleIndex.ts index 0ad943877fb..01e36ba138f 100644 --- a/packages/instantsearch.js/src/lib/stateMappings/singleIndex.ts +++ b/packages/instantsearch.js/src/lib/stateMappings/singleIndex.ts @@ -1,26 +1,2 @@ -import type { StateMapping, IndexUiState, UiState } from '../../types'; - -function getIndexStateWithoutConfigure( - uiState: TIndexUiState -): TIndexUiState { - const { configure, ...trackedUiState } = uiState; - return trackedUiState as TIndexUiState; -} - -export default function singleIndexStateMapping< - TUiState extends UiState = UiState ->( - indexName: keyof TUiState -): StateMapping { - return { - $$type: 'ais.singleIndex', - stateToRoute(uiState) { - return getIndexStateWithoutConfigure(uiState[indexName] || {}); - }, - routeToState(routeState = {} as TUiState[typeof indexName]) { - return { - [indexName]: getIndexStateWithoutConfigure(routeState), - } as unknown as TUiState; - }, - }; -} +export { singleIndex as default } from 'instantsearch-core'; +export type * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/lib/templating/prepareTemplateProps.ts b/packages/instantsearch.js/src/lib/templating/prepareTemplateProps.ts index 3eab971dc95..f13c8635b61 100644 --- a/packages/instantsearch.js/src/lib/templating/prepareTemplateProps.ts +++ b/packages/instantsearch.js/src/lib/templating/prepareTemplateProps.ts @@ -1,4 +1,4 @@ -import { uniq } from '../utils/uniq'; +import { uniq } from 'instantsearch-core'; import type { HoganHelpers, Templates } from '../../types'; import type { HoganOptions } from 'hogan.js'; diff --git a/packages/instantsearch.js/src/lib/templating/renderTemplate.ts b/packages/instantsearch.js/src/lib/templating/renderTemplate.ts index 7053bfaa643..3c76d4cf3b4 100644 --- a/packages/instantsearch.js/src/lib/templating/renderTemplate.ts +++ b/packages/instantsearch.js/src/lib/templating/renderTemplate.ts @@ -9,11 +9,11 @@ import { } from '../../helpers/components'; import type { Templates, HoganHelpers, TemplateParams } from '../../types'; +import type { HoganOptions, Template } from 'hogan.js'; import type { BindEventForHits, SendEventForHits, -} from '../utils/createSendEventForHits'; -import type { HoganOptions, Template } from 'hogan.js'; +} from 'instantsearch-core'; type TransformedHoganHelpers = { [helper: string]: () => (text: string) => string; diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForFacet-test.ts b/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForFacet-test.ts index fc9cdc1538e..1549fb391ed 100644 --- a/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForFacet-test.ts +++ b/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForFacet-test.ts @@ -4,14 +4,18 @@ import { castToJestMock } from '@instantsearch/testutils/castToJestMock'; import algoliasearchHelper from 'algoliasearch-helper'; +import { + createSendEventForFacet, + isFacetRefined, +} from 'instantsearch-core'; -import { createInstantSearch } from '../../../../test/createInstantSearch'; -import { createSendEventForFacet } from '../createSendEventForFacet'; -import { isFacetRefined } from '../isFacetRefined'; +import { createInstantSearch } from '../../../../../instantsearch-core/test/createInstantSearch'; import type { SearchClient } from '../../../types'; -jest.mock('../isFacetRefined', () => ({ isFacetRefined: jest.fn() })); +jest.mock('instantsearch-core/src/lib/utils/isFacetRefined', () => ({ + isFacetRefined: jest.fn(), +})); const createTestEnvironment = () => { const instantSearchInstance = createInstantSearch(); diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForHits-test.ts b/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForHits-test.ts index 5ca2a304a18..20710c7c210 100644 --- a/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForHits-test.ts +++ b/packages/instantsearch.js/src/lib/utils/__tests__/createSendEventForHits-test.ts @@ -2,12 +2,13 @@ * @jest-environment @instantsearch/testutils/jest-environment-jsdom.ts */ -import { createInstantSearch } from '../../../../test/createInstantSearch'; -import { deserializePayload } from '../../utils'; import { createBindEventForHits, createSendEventForHits, -} from '../createSendEventForHits'; + deserializePayload, +} from 'instantsearch-core'; + +import { createInstantSearch } from '../../../../../instantsearch-core/test/createInstantSearch'; import type { EscapedHits } from '../../../types'; diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/find-test.ts b/packages/instantsearch.js/src/lib/utils/__tests__/find-test.ts deleted file mode 100644 index 7efbd886787..00000000000 --- a/packages/instantsearch.js/src/lib/utils/__tests__/find-test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { find } from '../find'; - -describe('find', () => { - describe('with native array method', () => { - test('with empty array', () => { - const items: never[] = []; - const actual = find(items, (item) => item === 'hello'); - - expect(actual).toEqual(undefined); - }); - - test('with unknown item in array', () => { - const items = ['hey']; - const actual = find(items, (item) => item === 'hello'); - - expect(actual).toEqual(undefined); - }); - - test('with an array of strings', () => { - const items = ['hello', 'goodbye']; - const actual = find(items, (item) => item === 'hello'); - - expect(actual).toEqual('hello'); - }); - - test('with an array of objects', () => { - const items = [{ name: 'John' }, { name: 'Jane' }]; - const actual = find(items, (item) => item.name === 'John'); - - expect(actual).toEqual(items[0]); - }); - }); - - describe('with polyfill', () => { - // eslint-disable-next-line jest/unbound-method - const originalArrayFind = Array.prototype.find; - - beforeAll(() => { - // @ts-expect-error - delete Array.prototype.find; - }); - - afterAll(() => { - // eslint-disable-next-line no-extend-native - Array.prototype.find = originalArrayFind; - }); - - test('with empty array', () => { - const items: never[] = []; - const actual = find(items, (item) => item === 'hello'); - - expect(actual).toEqual(undefined); - }); - - test('with unknown item in array', () => { - const items = ['hey']; - const actual = find(items, (item) => item === 'hello'); - - expect(actual).toEqual(undefined); - }); - - test('with an array of strings', () => { - const items = ['hello', 'goodbye']; - const actual = find(items, (item) => item === 'hello'); - - expect(actual).toEqual('hello'); - }); - - test('with an array of objects', () => { - const items = [{ name: 'John' }, { name: 'Jane' }]; - const actual = find(items, (item) => item.name === 'John'); - - expect(actual).toEqual(items[0]); - }); - }); -}); diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/getWidgetAttribute-test.ts b/packages/instantsearch.js/src/lib/utils/__tests__/getWidgetAttribute-test.ts index fa54311ebac..7564655820a 100644 --- a/packages/instantsearch.js/src/lib/utils/__tests__/getWidgetAttribute-test.ts +++ b/packages/instantsearch.js/src/lib/utils/__tests__/getWidgetAttribute-test.ts @@ -3,7 +3,7 @@ */ import { getWidgetAttribute } from '..'; -import { createInitOptions } from '../../../../test/createWidget'; +import { createInitOptions } from '../../../../../instantsearch-core/test/createWidget'; import { connectRefinementList } from '../../../connectors'; import { hierarchicalMenu, diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/resolveSearchParameters-test.ts b/packages/instantsearch.js/src/lib/utils/__tests__/resolveSearchParameters-test.ts index 909f5762d19..476965ca025 100644 --- a/packages/instantsearch.js/src/lib/utils/__tests__/resolveSearchParameters-test.ts +++ b/packages/instantsearch.js/src/lib/utils/__tests__/resolveSearchParameters-test.ts @@ -2,9 +2,10 @@ * @jest-environment @instantsearch/testutils/jest-environment-jsdom.ts */ -import { createIndexInitOptions } from '../../../../test/createWidget'; +import { resolveSearchParameters } from 'instantsearch-core'; + +import { createIndexInitOptions } from '../../../../../instantsearch-core/test/createWidget'; import { index } from '../../../widgets'; -import { resolveSearchParameters } from '../resolveSearchParameters'; describe('mergeSearchParameters', () => { describe('1 level', () => { diff --git a/packages/instantsearch.js/src/lib/utils/index.ts b/packages/instantsearch.js/src/lib/utils/index.ts index 9e0c9b2ff9b..d59cbccc73f 100644 --- a/packages/instantsearch.js/src/lib/utils/index.ts +++ b/packages/instantsearch.js/src/lib/utils/index.ts @@ -1,54 +1,3 @@ -export * from './addWidgetId'; -export * from './capitalize'; -export * from './checkIndexUiState'; -export * from './checkRendering'; -export * from './clearRefinements'; -export * from './concatHighlightedParts'; -export * from './createConcurrentSafePromise'; -export * from './createSendEventForFacet'; -export * from './createSendEventForHits'; -export * from './setIndexHelperState'; -export * from './isIndexWidget'; -export * from './debounce'; -export * from './defer'; -export * from './documentation'; -export * from './escape-highlight'; -export * from './sendChatMessageFeedback'; -export * from './escape-html'; -export * from './escapeFacetValue'; -export * from './find'; -export * from './findIndex'; -export * from './geo-search'; -export * from './getAlgoliaAgent'; -export * from './getAppIdAndApiKey'; +export * from 'instantsearch-core'; export * from './getContainerNode'; -export * from './getHighlightedParts'; -export * from './getHighlightFromSiblings'; -export * from './getObjectType'; -export * from './getPropertyByPath'; -export * from './getRefinements'; -export * from './getWidgetAttribute'; -export * from './hits-absolute-position'; -export * from './hits-query-id'; -export * from './hydrateRecommendCache'; -export * from './hydrateSearchClient'; export * from './isDomElement'; -export * from './isEqual'; -export * from './isFacetRefined'; -export * from './isFiniteNumber'; -export * from './isPlainObject'; -export * from './isSpecialClick'; -export * from './walkIndex'; -export * from './isTwoPassWidget'; -export * from './logger'; -export * from './mergeSearchParameters'; -export * from './omit'; -export * from './noop'; -export * from './range'; -export * from './render-args'; -export * from './resolveSearchParameters'; -export * from './reverseHighlightedParts'; -export * from './safelyRunOnBrowser'; -export * from './serializer'; -export * from './toArray'; -export * from './uniq'; diff --git a/packages/instantsearch.js/src/lib/voiceSearchHelper/index.ts b/packages/instantsearch.js/src/lib/voiceSearchHelper/index.ts index f33514b4a93..fe3212b3aab 100644 --- a/packages/instantsearch.js/src/lib/voiceSearchHelper/index.ts +++ b/packages/instantsearch.js/src/lib/voiceSearchHelper/index.ts @@ -1,131 +1,2 @@ -// `SpeechRecognition` is an API used on the browser so we can safely disable -// the `window` check. -/* eslint-disable no-restricted-globals */ -/* global SpeechRecognition SpeechRecognitionEvent */ -import type { - CreateVoiceSearchHelper, - Status, - VoiceListeningState, -} from './types'; - -const createVoiceSearchHelper: CreateVoiceSearchHelper = - function createVoiceSearchHelper({ - searchAsYouSpeak, - language, - onQueryChange, - onStateChange, - }) { - const SpeechRecognitionAPI: new () => SpeechRecognition = - (window as any).webkitSpeechRecognition || - (window as any).SpeechRecognition; - const getDefaultState = (status: Status): VoiceListeningState => ({ - status, - transcript: '', - isSpeechFinal: false, - errorCode: undefined, - }); - let state: VoiceListeningState = getDefaultState('initial'); - let recognition: SpeechRecognition | undefined; - - const isBrowserSupported = (): boolean => Boolean(SpeechRecognitionAPI); - - const isListening = (): boolean => - state.status === 'askingPermission' || - state.status === 'waiting' || - state.status === 'recognizing'; - - const setState = (newState: Partial = {}): void => { - state = { ...state, ...newState }; - onStateChange(); - }; - - const getState = (): VoiceListeningState => state; - - const resetState = (status: Status = 'initial'): void => { - setState(getDefaultState(status)); - }; - - const onStart = (): void => { - setState({ - status: 'waiting', - }); - }; - - const onError = (event: Event): void => { - setState({ status: 'error', errorCode: (event as any).error }); - }; - - const onResult = (event: SpeechRecognitionEvent): void => { - setState({ - status: 'recognizing', - transcript: - (event.results[0] && - event.results[0][0] && - event.results[0][0].transcript) || - '', - isSpeechFinal: event.results[0] && event.results[0].isFinal, - }); - if (searchAsYouSpeak && state.transcript) { - onQueryChange(state.transcript); - } - }; - - const onEnd = (): void => { - if (!state.errorCode && state.transcript && !searchAsYouSpeak) { - onQueryChange(state.transcript); - } - if (state.status !== 'error') { - setState({ status: 'finished' }); - } - }; - - const startListening = (): void => { - recognition = new SpeechRecognitionAPI(); - if (!recognition) { - return; - } - resetState('askingPermission'); - recognition.interimResults = true; - - if (language) { - recognition.lang = language; - } - - recognition.addEventListener('start', onStart); - recognition.addEventListener('error', onError); - recognition.addEventListener('result', onResult); - recognition.addEventListener('end', onEnd); - recognition.start(); - }; - - const dispose = (): void => { - if (!recognition) { - return; - } - recognition.stop(); - recognition.removeEventListener('start', onStart); - recognition.removeEventListener('error', onError); - recognition.removeEventListener('result', onResult); - recognition.removeEventListener('end', onEnd); - recognition = undefined; - }; - - const stopListening = (): void => { - dispose(); - // Because `dispose` removes event listeners, `end` listener is not called. - // So we're setting the `status` as `finished` here. - // If we don't do it, it will be still `waiting` or `recognizing`. - resetState('finished'); - }; - - return { - getState, - isBrowserSupported, - isListening, - startListening, - stopListening, - dispose, - }; - }; - -export default createVoiceSearchHelper; +export { createVoiceSearchHelper as default } from 'instantsearch-core'; +export type * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/middlewares/__tests__/createInsightsMiddleware.ts b/packages/instantsearch.js/src/middlewares/__tests__/createInsightsMiddleware.ts index 11e6295e65e..e11fcaacc5b 100644 --- a/packages/instantsearch.js/src/middlewares/__tests__/createInsightsMiddleware.ts +++ b/packages/instantsearch.js/src/middlewares/__tests__/createInsightsMiddleware.ts @@ -14,7 +14,7 @@ import { wait } from '@instantsearch/testutils/wait'; import { fireEvent } from '@testing-library/dom'; import { createInsightsMiddleware } from '..'; -import { createInstantSearch } from '../../../test/createInstantSearch'; +import { createInstantSearch } from '../../../../instantsearch-core/test/createInstantSearch'; import { connectSearchBox } from '../../connectors'; import instantsearch from '../../index.es'; import { history } from '../../lib/routers'; diff --git a/packages/instantsearch.js/src/middlewares/__tests__/createMetadataMiddleware.ts b/packages/instantsearch.js/src/middlewares/__tests__/createMetadataMiddleware.ts index 6207434f9c2..0da701d4786 100644 --- a/packages/instantsearch.js/src/middlewares/__tests__/createMetadataMiddleware.ts +++ b/packages/instantsearch.js/src/middlewares/__tests__/createMetadataMiddleware.ts @@ -9,9 +9,9 @@ import algoliasearchV4 from 'algoliasearch-v4'; import { algoliasearch as algoliasearchV5 } from 'algoliasearch-v5'; import { createMetadataMiddleware } from '..'; +import { isMetadataEnabled } from '..'; import instantsearch from '../..'; import { configure, hits, index, pagination, searchBox } from '../../widgets'; -import { isMetadataEnabled } from '../createMetadataMiddleware'; declare global { interface Navigator { @@ -271,6 +271,7 @@ describe('createMetadataMiddleware', () => { expect.stringMatching(/Algolia for JavaScript \(5\..*\)/), expect.stringMatching(/Search \(5\..*\)/), expect.stringMatching(/Node.js \(.*\)/), + expect.stringMatching(/instantsearch-core \(.*\)/), expect.stringMatching(/instantsearch.js \(4\..*\)/), expect.stringMatching(/JS Helper \(3\..*\)/), ]); @@ -312,6 +313,7 @@ describe('createMetadataMiddleware', () => { expect.stringMatching(/Algolia for JavaScript \(5\..*\)/), expect.stringMatching(/Search \(5\..*\)/), expect.stringMatching(/Node.js \(.*\)/), + expect.stringMatching(/instantsearch-core \(.*\)/), expect.stringMatching(/instantsearch.js \(4\..*\)/), expect.stringMatching(/JS Helper \(3\..*\)/), 'test (cool)', @@ -351,6 +353,7 @@ describe('createMetadataMiddleware', () => { ).toEqual([ expect.stringMatching(/Algolia for JavaScript \(4\..*\)/), expect.stringMatching(/Node.js \(.*\)/), + expect.stringMatching(/instantsearch-core \(.*\)/), expect.stringMatching(/instantsearch.js \(4\..*\)/), expect.stringMatching(/JS Helper \(3\..*\)/), ]); @@ -391,6 +394,7 @@ describe('createMetadataMiddleware', () => { ).toEqual([ expect.stringMatching(/Algolia for JavaScript \(4\..*\)/), expect.stringMatching(/Node.js \(.*\)/), + expect.stringMatching(/instantsearch-core \(.*\)/), expect.stringMatching(/instantsearch.js \(4\..*\)/), expect.stringMatching(/JS Helper \(3\..*\)/), 'test (cool)', @@ -430,6 +434,7 @@ describe('createMetadataMiddleware', () => { ).toEqual([ expect.stringMatching(/Algolia for JavaScript \(3\..*\)/), expect.stringMatching(/Node.js \(.*\)/), + expect.stringMatching(/instantsearch-core \(.*\)/), expect.stringMatching(/instantsearch.js \(4\..*\)/), expect.stringMatching(/JS Helper \(3\..*\)/), ]); @@ -470,6 +475,7 @@ describe('createMetadataMiddleware', () => { ).toEqual([ expect.stringMatching(/Algolia for JavaScript \(3\..*\)/), expect.stringMatching(/Node.js \(.*\)/), + expect.stringMatching(/instantsearch-core \(.*\)/), expect.stringMatching(/instantsearch.js \(4\..*\)/), expect.stringMatching(/JS Helper \(3\..*\)/), 'test (cool)', diff --git a/packages/instantsearch.js/src/middlewares/index.ts b/packages/instantsearch.js/src/middlewares/index.ts index a44215a78bc..ef53583fdb7 100644 --- a/packages/instantsearch.js/src/middlewares/index.ts +++ b/packages/instantsearch.js/src/middlewares/index.ts @@ -1,3 +1,15 @@ -export * from './createInsightsMiddleware'; -export * from './createRouterMiddleware'; -export * from './createMetadataMiddleware'; +export { + createInsightsMiddleware, + createMetadataMiddleware, + createRouterMiddleware, + isMetadataEnabled, +} from 'instantsearch-core'; +export type { + CreateInsightsMiddleware, + InsightsClient, + InsightsClientWithGlobals, + InsightsEvent, + InsightsMethod, + InsightsProps, + RouterProps, +} from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/types/index.ts b/packages/instantsearch.js/src/types/index.ts index 0faeb9de897..c5945f71e45 100644 --- a/packages/instantsearch.js/src/types/index.ts +++ b/packages/instantsearch.js/src/types/index.ts @@ -1,25 +1,6 @@ -// internal -export * from './utils'; +// shared InstantSearch types +export * from 'instantsearch-core'; -// Algolia-related -// eslint-disable-next-line import/export -export * from './algoliasearch'; -export * from './results'; -export * from './recommend'; - -// component-related +// InstantSearch.js-specific types export * from './component'; - -// instantsearch-related -export * from './instantsearch'; -export * from './middleware'; -export * from './router'; -export * from './insights'; - -// widget-related -export * from './connector'; -export * from './widget-factory'; -export * from './widget'; -export * from './ui-state'; -export * from './render-state'; export * from './templates'; diff --git a/packages/instantsearch.js/src/types/widget-factory.ts b/packages/instantsearch.js/src/types/widget-factory.ts index f55c13f7aee..1b4fd8425dc 100644 --- a/packages/instantsearch.js/src/types/widget-factory.ts +++ b/packages/instantsearch.js/src/types/widget-factory.ts @@ -1,21 +1 @@ -import type { UnknownWidgetParams, Widget, WidgetDescription } from './widget'; - -/** - * The function that creates a new widget. - */ -export type WidgetFactory< - TWidgetDescription extends WidgetDescription, - TConnectorParams extends UnknownWidgetParams, - TWidgetParams extends UnknownWidgetParams -> = ( - /** - * The params of the widget. - */ - widgetParams: TWidgetParams & TConnectorParams -) => Widget< - TWidgetDescription & { - widgetParams: TConnectorParams; - } ->; - -export type UnknownWidgetFactory = WidgetFactory<{ $$type: string }, any, any>; +export * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/widgets/answers/__tests__/answers-test.ts b/packages/instantsearch.js/src/widgets/answers/__tests__/answers-test.ts index b5a335f4453..3ed73f38512 100644 --- a/packages/instantsearch.js/src/widgets/answers/__tests__/answers-test.ts +++ b/packages/instantsearch.js/src/widgets/answers/__tests__/answers-test.ts @@ -8,7 +8,7 @@ import { wait } from '@instantsearch/testutils/wait'; import { fireEvent } from '@testing-library/preact'; import algoliasearchHelper from 'algoliasearch-helper'; -import { createInitOptions } from '../../../../test/createWidget'; +import { createInitOptions } from '../../../../../instantsearch-core/test/createWidget'; import instantsearch from '../../../index.es'; import searchBox from '../../search-box/search-box'; import answers from '../answers'; diff --git a/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx b/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx index 62576c88f64..becd08f5632 100644 --- a/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx +++ b/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx @@ -1,5 +1,12 @@ /** @jsx h */ +import { + getPromptSuggestionHits, + isChatBusy, + isPromptSuggestion, + openChat, + type ChatRenderState, +} from 'instantsearch-core'; import { createAutocompleteComponent, createAutocompleteDetachedContainerComponent, @@ -15,18 +22,18 @@ import { createAutocompleteStorage, createAutocompleteSuggestionComponent, cx, - getPromptSuggestionHits, - isPromptSuggestion, } from 'instantsearch-ui-components'; import { Fragment, h, render } from 'preact'; import { useEffect, useId, useMemo, useRef, useState } from 'preact/hooks'; import TemplateComponent from '../../components/Template/Template'; -import connectFeeds from '../../connectors/feeds/connectFeeds'; import { createFeedContainer } from '../../connectors/feeds/FeedContainer'; -import { connectAutocomplete, connectSearchBox } from '../../connectors/index'; +import { + connectAutocomplete, + connectFeeds, + connectSearchBox, +} from '../../connectors/index'; import { Highlight, ReverseHighlight } from '../../helpers/components'; -import { isChatBusy, openChat } from '../../lib/chat'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -45,8 +52,7 @@ import type { AutocompleteRenderState, AutocompleteWidgetDescription, TransformItemsIndicesConfig, -} from '../../connectors/autocomplete/connectAutocomplete'; -import type { ChatRenderState } from '../../connectors/chat/connectChat'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { BaseHit, @@ -832,7 +838,7 @@ function AutocompleteWrapper({ query={localQuery} inputProps={{ ...inputProps, - onFocus: (event) => { + onFocus: (event: unknown) => { activate(); (inputProps as { onFocus?: (event: unknown) => void }).onFocus?.( event diff --git a/packages/instantsearch.js/src/widgets/breadcrumb/breadcrumb.tsx b/packages/instantsearch.js/src/widgets/breadcrumb/breadcrumb.tsx index 8f0b2f69c12..8976b8555c1 100644 --- a/packages/instantsearch.js/src/widgets/breadcrumb/breadcrumb.tsx +++ b/packages/instantsearch.js/src/widgets/breadcrumb/breadcrumb.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import Breadcrumb from '../../components/Breadcrumb/Breadcrumb'; -import connectBreadcrumb from '../../connectors/breadcrumb/connectBreadcrumb'; +import { connectBreadcrumb } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -22,7 +22,7 @@ import type { BreadcrumbWidgetDescription, BreadcrumbConnectorParams, BreadcrumbRenderState, -} from '../../connectors/breadcrumb/connectBreadcrumb'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { WidgetFactory, Template, Renderer } from '../../types'; diff --git a/packages/instantsearch.js/src/widgets/chat-trigger/chat-trigger.tsx b/packages/instantsearch.js/src/widgets/chat-trigger/chat-trigger.tsx index 43d2687729f..06b1260f09a 100644 --- a/packages/instantsearch.js/src/widgets/chat-trigger/chat-trigger.tsx +++ b/packages/instantsearch.js/src/widgets/chat-trigger/chat-trigger.tsx @@ -1,21 +1,21 @@ /** @jsx h */ +import { connectChatTrigger } from 'instantsearch-core'; import { createChatToggleButtonComponent } from 'instantsearch-ui-components'; import { h, Fragment, render } from 'preact'; import TemplateComponent from '../../components/Template/Template'; -import connectChatTrigger from '../../connectors/chat/connectChatTrigger'; import { prepareTemplateProps } from '../../lib/templating'; import { getContainerNode, createDocumentationMessageGenerator, } from '../../lib/utils'; +import type { RendererOptions, Template } from '../../types'; import type { ChatTriggerConnectorParams, ChatTriggerRenderState, -} from '../../connectors/chat/connectChatTrigger'; -import type { RendererOptions, Template } from '../../types'; +} from 'instantsearch-core'; import type { ChatToggleButtonProps, Pragma, diff --git a/packages/instantsearch.js/src/widgets/chat/chat.tsx b/packages/instantsearch.js/src/widgets/chat/chat.tsx index d8a489c739d..50074b70132 100644 --- a/packages/instantsearch.js/src/widgets/chat/chat.tsx +++ b/packages/instantsearch.js/src/widgets/chat/chat.tsx @@ -1,5 +1,14 @@ /** @jsx h */ +import { + SearchIndexToolType, + RecommendToolType, + MemorizeToolType, + MemorySearchToolType, + PonderToolType, + DisplayResultsToolType, +} from 'instantsearch-core'; +import { connectChat } from 'instantsearch-core'; import { ArrowRightIcon, ChevronLeftIcon, @@ -12,15 +21,6 @@ import { Component, Fragment, h, render } from 'preact'; import { useEffect, useMemo, useState } from 'preact/hooks'; import TemplateComponent from '../../components/Template/Template'; -import connectChat from '../../connectors/chat/connectChat'; -import { - SearchIndexToolType, - RecommendToolType, - MemorizeToolType, - MemorySearchToolType, - PonderToolType, - DisplayResultsToolType, -} from '../../lib/chat'; import { prepareTemplateProps } from '../../lib/templating'; import { useStickToBottom } from '../../lib/useStickToBottom'; import { @@ -34,11 +34,6 @@ import { carousel } from '../../templates'; import { createDisplayResultsTool } from './display-results-tool'; import type { TemplateProps } from '../../components/Template/Template'; -import type { - ChatRenderState, - ChatConnectorParams, - ChatWidgetDescription, -} from '../../connectors/chat/connectChat'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { WidgetFactory, @@ -51,13 +46,23 @@ import type { IndexWidget, } from '../../types'; import type { SearchParameters } from 'algoliasearch-helper'; +import type { + ChatConnectorParams, + ChatRenderState, + ChatStatus, + ChatWidgetDescription, + ClientSideToolComponentProps, + ClientSideTools, + SearchToolInput, + UIMessage, + UserClientSideTool, +} from 'instantsearch-core'; import type { ChatClassNames, ChatHeaderProps, ChatHeaderTranslations, ChatLayoutOwnProps, ChatMessageActionProps, - ChatMessageBase, ChatMessageErrorProps, ChatEmptyProps, ChatMessageLoaderProps, @@ -65,12 +70,7 @@ import type { ChatMessagesTranslations, ChatPromptProps, ChatPromptTranslations, - ChatStatus, - ClientSideToolComponentProps, - ClientSideTools, RecordWithObjectID, - SearchToolInput, - UserClientSideTool, } from 'instantsearch-ui-components'; import type { ComponentProps } from 'preact'; @@ -311,7 +311,7 @@ type ChatWrapperProps = { cssClasses: ChatCSSClasses; chatOpen: boolean; setChatOpen: (open: boolean) => void; - chatMessages: ChatMessageBase[]; + chatMessages: UIMessage[]; indexUiState: IndexUiState; setIndexUiState: IndexWidget['setIndexUiState']; chatStatus: ChatStatus; @@ -1172,7 +1172,7 @@ export type ChatTemplates = BaseHit> = */ actions: Template<{ actions: ChatMessageActionProps[]; - message: ChatMessageBase; + message: UIMessage; }>; /** diff --git a/packages/instantsearch.js/src/widgets/chat/display-results-tool.tsx b/packages/instantsearch.js/src/widgets/chat/display-results-tool.tsx index 8caebb5df0a..9c361e33fa2 100644 --- a/packages/instantsearch.js/src/widgets/chat/display-results-tool.tsx +++ b/packages/instantsearch.js/src/widgets/chat/display-results-tool.tsx @@ -14,9 +14,9 @@ import { carousel } from '../../templates'; import type { ChatTemplates, - ClientSideToolTemplateData, Tool as UserClientSideToolWithTemplate, } from './chat'; +import type { ClientSideToolComponentProps } from 'instantsearch-core'; import type { RecordWithObjectID } from 'instantsearch-ui-components'; export function createDisplayResultsTool< @@ -33,7 +33,7 @@ export function createDisplayResultsTool< const Button = createButtonComponent({ createElement: h }); function DisplayResultsLayoutComponent( - toolProps: ClientSideToolTemplateData + toolProps: ClientSideToolComponentProps ) { return ( ->[1]; -type LocalWidgetSearchParametersOptions = WidgetSearchParametersOptions & { - initialSearchParameters: SearchParameters; -}; -type LocalWidgetRecommendParametersOptions = WidgetSearchParametersOptions & { - initialRecommendParameters: RecommendParameters; -}; - -export type IndexWidgetDescription = { - $$type: IndexWidgetType; - $$widgetType: IndexWidgetType; -}; - -export type IndexWidget = Omit< - Widget, - 'getWidgetUiState' | 'getWidgetState' -> & { - getIndexName: () => string; - getIndexId: () => string; - getHelper: () => Helper | null; - getResults: () => SearchResults | null; - getResultsForWidget: ( - widget: IndexWidget | Widget - ) => SearchResults | RecommendResponse | null; - getPreviousState: () => SearchParameters | null; - getScopedResults: () => ScopedResult[]; - getParent: () => IndexWidget | null; - getWidgets: () => Array; - createURL: ( - nextState: SearchParameters | ((state: IndexUiState) => IndexUiState) - ) => string; - - addWidgets: ( - widgets: Array> - ) => IndexWidget; - removeWidgets: ( - widgets: Array - ) => IndexWidget; - - init: (options: IndexInitOptions) => void; - render: (options: IndexRenderOptions) => void; - dispose: () => void; - /** - * @deprecated - */ - getWidgetState: (uiState: UiState) => UiState; - getWidgetUiState: ( - uiState: TSpecificUiState - ) => TSpecificUiState; - getWidgetSearchParameters: ( - searchParameters: SearchParameters, - searchParametersOptions: { uiState: IndexUiState } - ) => SearchParameters; - /** - * Set this index' UI state back to the state defined by the widgets. - * Can only be called after `init`. - */ - refreshUiState: () => void; - /** - * Set this index' UI state and search. This is the equivalent of calling - * a spread `setUiState` on the InstantSearch instance. - * Can only be called after `init`. - */ - setIndexUiState: ( - indexUiState: - | TUiState[string] - | ((previousIndexUiState: TUiState[string]) => TUiState[string]) - ) => void; - /** - * This index is isolated, meaning it will not be merged with the main - * helper's state. - * @private - */ - _isolated: boolean; - /** - * Schedules a search for this index only. - * @private - */ - scheduleLocalSearch: () => void; -}; - -/** - * This is the same content as helper._change / setState, but allowing for extra - * UiState to be synchronized. - * see: https://github.com/algolia/algoliasearch-helper-js/blob/6b835ffd07742f2d6b314022cce6848f5cfecd4a/src/algoliasearch.helper.js#L1311-L1324 - */ -function privateHelperSetState( - helper: AlgoliaSearchHelper, - { - state, - recommendState, - isPageReset, - _uiState, - }: { - state: SearchParameters; - recommendState: RecommendParameters; - isPageReset?: boolean; - _uiState?: IndexUiState; - } -) { - if (state !== helper.state) { - helper.state = state; - - helper.emit('change', { - state: helper.state, - results: helper.lastResults, - isPageReset, - _uiState, - }); - } - - if (recommendState !== helper.recommendState) { - helper.recommendState = recommendState; - - // eslint-disable-next-line no-warning-comments - // TODO: emit "change" event when events for Recommend are implemented - } -} - -type WidgetUiStateOptions = Parameters< - NonNullable ->[1]; - -function getLocalWidgetsUiState( - widgets: Array, - widgetStateOptions: WidgetUiStateOptions, - initialUiState: IndexUiState = {} -) { - return widgets.reduce((uiState, widget) => { - if (isIndexWidget(widget)) { - return uiState; - } - - if (!widget.getWidgetUiState && !widget.getWidgetState) { - return uiState; - } - - if (widget.getWidgetUiState) { - return widget.getWidgetUiState(uiState, widgetStateOptions); - } - - return widget.getWidgetState!(uiState, widgetStateOptions); - }, initialUiState); -} - -function getLocalWidgetsSearchParameters( - widgets: Array, - widgetSearchParametersOptions: LocalWidgetSearchParametersOptions -): SearchParameters { - const { initialSearchParameters, ...rest } = widgetSearchParametersOptions; - - return widgets.reduce((state, widget) => { - if (!widget.getWidgetSearchParameters || isIndexWidget(widget)) { - return state; - } - - if (widget.dependsOn === 'search' && widget.getWidgetParameters) { - return widget.getWidgetParameters(state, rest); - } - - return widget.getWidgetSearchParameters(state, rest); - }, initialSearchParameters); -} - -function getLocalWidgetsRecommendParameters( - widgets: Array, - widgetRecommendParametersOptions: LocalWidgetRecommendParametersOptions -): RecommendParameters { - const { initialRecommendParameters, ...rest } = - widgetRecommendParametersOptions; - - return widgets.reduce((state, widget) => { - if ( - !isIndexWidget(widget) && - widget.dependsOn === 'recommend' && - widget.getWidgetParameters - ) { - return widget.getWidgetParameters(state, rest); - } - return state; - }, initialRecommendParameters); -} - -function resetPageFromWidgets(widgets: Array): void { - const indexWidgets = widgets.filter(isIndexWidget); - - if (indexWidgets.length === 0) { - return; - } - - indexWidgets.forEach((widget) => { - const widgetHelper = widget.getHelper()!; - - privateHelperSetState(widgetHelper, { - state: widgetHelper.state.resetPage(), - recommendState: widgetHelper.recommendState, - isPageReset: true, - }); - - resetPageFromWidgets(widget.getWidgets()); - }); -} - -function resolveScopedResultsFromWidgets( - widgets: Array -): ScopedResult[] { - const indexWidgets = widgets.filter(isIndexWidget); - - return indexWidgets.reduce((scopedResults, current) => { - return scopedResults.concat( - { - indexId: current.getIndexId(), - results: current.getResults()!, - helper: current.getHelper()!, - }, - ...resolveScopedResultsFromWidgets(current.getWidgets()) - ); - }, []); -} - -const index = (widgetParams: IndexWidgetParams): IndexWidget => { - if ( - widgetParams === undefined || - (widgetParams.indexName === undefined && - !widgetParams.EXPERIMENTAL_isolated) - ) { - throw new Error(withUsage('The `indexName` option is required.')); - } - - // When isolated=true, we use an empty string as the default indexName. - // This is intentional: isolated indices do not require a real index name. - const { - indexName = '', - indexId = indexName, - EXPERIMENTAL_isolated: isolated = false, - } = widgetParams; - - let localWidgets: Array = []; - let localUiState: IndexUiState = {}; - let localInstantSearchInstance: InstantSearch | null = null; - let localParent: IndexWidget | null = null; - let helper: Helper | null = null; - let derivedHelper: DerivedHelper | null = null; - let lastValidSearchParameters: SearchParameters | null = null; - let hasRecommendWidget: boolean = false; - let hasSearchWidget: boolean = false; - - return { - $$type: 'ais.index', - $$widgetType: 'ais.index', - - _isolated: isolated, - - getIndexName() { - return indexName; - }, - - getIndexId() { - return indexId; - }, - - getHelper() { - return helper; - }, - - getResults() { - if (!derivedHelper?.lastResults) return null; - - // To make the UI optimistic, we patch the state to display to the current - // one instead of the one associated with the latest results. - // This means user-driven UI changes (e.g., checked checkbox) are reflected - // immediately instead of waiting for Algolia to respond, regardless of - // the status of the network request. - derivedHelper.lastResults._state = helper!.state; - - return derivedHelper.lastResults; - }, - - getResultsForWidget(widget) { - if ( - widget.dependsOn !== 'recommend' || - isIndexWidget(widget) || - widget.$$id === undefined - ) { - return this.getResults(); - } - - if (!helper?.lastRecommendResults) { - return null; - } - - return helper.lastRecommendResults[widget.$$id]; - }, - - getPreviousState() { - return lastValidSearchParameters; - }, - - getScopedResults() { - const widgetParent = this.getParent(); - let widgetSiblings; - - if (widgetParent) { - widgetSiblings = widgetParent.getWidgets(); - } else if (indexName.length === 0) { - // The widget is the root but has no index name: - // we resolve results from its children index widgets - widgetSiblings = this.getWidgets(); - } else { - // The widget is the root and has an index name: - // we consider itself as the only sibling - widgetSiblings = [this]; - } - - return resolveScopedResultsFromWidgets(widgetSiblings); - }, - - getParent() { - return isolated ? null : localParent; - }, - - createURL( - nextState: SearchParameters | ((state: IndexUiState) => IndexUiState) - ) { - if (typeof nextState === 'function') { - return localInstantSearchInstance!._createURL({ - [indexId]: nextState(localUiState), - }); - } - return localInstantSearchInstance!._createURL({ - [indexId]: getLocalWidgetsUiState(localWidgets, { - searchParameters: nextState, - helper: helper!, - }), - }); - }, - - scheduleLocalSearch: defer(() => { - if (isolated) { - helper?.search(); - } - }), - - getWidgets() { - return localWidgets; - }, - - addWidgets(widgets) { - if (!Array.isArray(widgets)) { - throw new Error( - withUsage('The `addWidgets` method expects an array of widgets.') - ); - } - const flatWidgets = widgets.reduce>( - (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), - [] - ); - - if ( - flatWidgets.some( - (widget) => - typeof widget.init !== 'function' && - typeof widget.render !== 'function' - ) - ) { - throw new Error( - withUsage( - 'The widget definition expects a `render` and/or an `init` method.' - ) - ); - } - - flatWidgets.forEach((widget) => { - widget.parent = this; - if (isIndexWidget(widget)) { - return; - } - - if (localInstantSearchInstance && widget.dependsOn === 'recommend') { - localInstantSearchInstance._hasRecommendWidget = true; - } else if (localInstantSearchInstance) { - localInstantSearchInstance._hasSearchWidget = true; - } else if (widget.dependsOn === 'recommend') { - hasRecommendWidget = true; - } else { - hasSearchWidget = true; - } - - addWidgetId(widget); - }); - - localWidgets = localWidgets.concat(flatWidgets); - if (localInstantSearchInstance && Boolean(flatWidgets.length)) { - privateHelperSetState(helper!, { - state: getLocalWidgetsSearchParameters(localWidgets, { - uiState: localUiState, - initialSearchParameters: helper!.state, - }), - recommendState: getLocalWidgetsRecommendParameters(localWidgets, { - uiState: localUiState, - initialRecommendParameters: helper!.recommendState, - }), - _uiState: localUiState, - }); - - // We compute the render state before calling `init` in a separate loop - // to construct the whole render state object that is then passed to - // `init`. - flatWidgets.forEach((widget) => { - if (widget.getRenderState) { - const renderState = widget.getRenderState( - localInstantSearchInstance!.renderState[this.getIndexId()] || {}, - createInitArgs( - localInstantSearchInstance!, - this, - localInstantSearchInstance!._initialUiState - ) - ); - - storeRenderState({ - renderState, - instantSearchInstance: localInstantSearchInstance!, - parent: this, - }); - } - }); - - flatWidgets.forEach((widget) => { - if (widget.init) { - widget.init( - createInitArgs( - localInstantSearchInstance!, - this, - localInstantSearchInstance!._initialUiState - ) - ); - } - }); - - if (isolated) { - this.scheduleLocalSearch(); - } else { - localInstantSearchInstance.scheduleSearch(); - } - } - - return this; - }, - - removeWidgets(widgets) { - if (!Array.isArray(widgets)) { - throw new Error( - withUsage('The `removeWidgets` method expects an array of widgets.') - ); - } - const flatWidgets = widgets.reduce>( - (acc, w) => acc.concat(Array.isArray(w) ? w : [w]), - [] - ); - - if (flatWidgets.some((widget) => typeof widget.dispose !== 'function')) { - throw new Error( - withUsage('The widget definition expects a `dispose` method.') - ); - } - - localWidgets = localWidgets.filter( - (widget) => flatWidgets.indexOf(widget) === -1 - ); - - localWidgets.forEach((widget) => { - widget.parent = undefined; - if (isIndexWidget(widget)) { - return; - } - - if (localInstantSearchInstance && widget.dependsOn === 'recommend') { - localInstantSearchInstance._hasRecommendWidget = true; - } else if (localInstantSearchInstance) { - localInstantSearchInstance._hasSearchWidget = true; - } else if (widget.dependsOn === 'recommend') { - hasRecommendWidget = true; - } else { - hasSearchWidget = true; - } - }); - - if (localInstantSearchInstance && Boolean(flatWidgets.length)) { - const { cleanedSearchState, cleanedRecommendState } = - flatWidgets.reduce( - (states, widget) => { - // the `dispose` method exists at this point we already assert it - const next = widget.dispose!({ - helper: helper!, - state: states.cleanedSearchState, - recommendState: states.cleanedRecommendState, - parent: this, - }); - - if (next instanceof algoliasearchHelper.RecommendParameters) { - states.cleanedRecommendState = next; - } else if (next) { - states.cleanedSearchState = next; - } - - return states; - }, - { - cleanedSearchState: helper!.state, - cleanedRecommendState: helper!.recommendState, - } - ); - - const newState = localInstantSearchInstance.future - .preserveSharedStateOnUnmount - ? getLocalWidgetsSearchParameters(localWidgets, { - uiState: localUiState, - initialSearchParameters: new algoliasearchHelper.SearchParameters( - { - index: this.getIndexName(), - } - ), - }) - : getLocalWidgetsSearchParameters(localWidgets, { - uiState: getLocalWidgetsUiState(localWidgets, { - searchParameters: cleanedSearchState, - helper: helper!, - }), - initialSearchParameters: cleanedSearchState, - }); - - localUiState = getLocalWidgetsUiState(localWidgets, { - searchParameters: newState, - helper: helper!, - }); - - helper!.setState(newState); - helper!.recommendState = cleanedRecommendState; - - if (localWidgets.length) { - if (isolated) { - this.scheduleLocalSearch(); - } else { - localInstantSearchInstance.scheduleSearch(); - } - } - } - - return this; - }, - - init({ instantSearchInstance, parent, uiState }: IndexInitOptions) { - if (helper !== null) { - // helper is already initialized, therefore we do not need to set up - // any listeners - return; - } - - localInstantSearchInstance = instantSearchInstance; - localParent = parent; - localUiState = uiState[indexId] || {}; - - // The `mainHelper` is already defined at this point. The instance is created - // inside InstantSearch at the `start` method, which occurs before the `init` - // step. - const mainHelper = instantSearchInstance.mainHelper!; - const parameters = getLocalWidgetsSearchParameters(localWidgets, { - uiState: localUiState, - initialSearchParameters: new algoliasearchHelper.SearchParameters({ - index: indexName, - }), - }); - const recommendParameters = getLocalWidgetsRecommendParameters( - localWidgets, - { - uiState: localUiState, - initialRecommendParameters: - new algoliasearchHelper.RecommendParameters(), - } - ); - - // This Helper is only used for state management we do not care about the - // `searchClient`. Only the "main" Helper created at the `InstantSearch` - // level is aware of the client. - helper = algoliasearchHelper( - mainHelper.getClient(), - parameters.index, - parameters - ); - helper.recommendState = recommendParameters; - - // We forward the call to `search` to the "main" instance of the Helper - // which is responsible for managing the queries (it's the only one that is - // aware of the `searchClient`). - helper.search = () => { - if (isolated) { - instantSearchInstance.status = 'loading'; - this.render({ instantSearchInstance }); - return instantSearchInstance.compositionID - ? helper!.searchWithComposition() - : helper!.searchOnlyWithDerivedHelpers(); - } - - if (instantSearchInstance.onStateChange) { - instantSearchInstance.onStateChange({ - uiState: instantSearchInstance.mainIndex.getWidgetUiState({}), - setUiState: (nextState) => - instantSearchInstance.setUiState(nextState, false), - }); - - // We don't trigger a search when controlled because it becomes the - // responsibility of `setUiState`. - return mainHelper; - } - - return mainHelper.search(); - }; - - helper.searchWithoutTriggeringOnStateChange = () => { - return mainHelper.search(); - }; - - // We use the same pattern for the `searchForFacetValues`. - helper.searchForFacetValues = ( - facetName, - facetValue, - maxFacetHits, - userState - ) => { - const state = mergeSearchParameters( - mainHelper.state, - ...resolveSearchParameters(this) - ).setQueryParameters(userState!); - - return mainHelper.searchForFacetValues( - facetName, - facetValue, - maxFacetHits, - state - ); - }; - - const isolatedHelper = indexName - ? helper - : algoliasearchHelper({} as SearchClient, '__empty_index__', {}); - const derivingHelper = isolated - ? isolatedHelper - : nearestIsolatedHelper(parent, mainHelper); - - derivedHelper = derivingHelper.derive( - () => - mergeSearchParameters( - mainHelper.state, - ...resolveSearchParameters(this) - ), - () => this.getHelper()!.recommendState - ); - - const indexInitialResults = - instantSearchInstance._initialResults?.[this.getIndexId()]; - - if (indexInitialResults?.results) { - // We restore the shape of the results provided to the instance to respect - // the helper's structure. - const results = new algoliasearchHelper.SearchResults( - new algoliasearchHelper.SearchParameters(indexInitialResults.state), - indexInitialResults.results - ); - - derivedHelper.lastResults = results; - helper.lastResults = results; - } - - if (indexInitialResults?.recommendResults) { - const recommendResults = new algoliasearchHelper.RecommendResults( - new algoliasearchHelper.RecommendParameters({ - params: indexInitialResults.recommendResults.params, - }), - indexInitialResults.recommendResults.results - ); - derivedHelper.lastRecommendResults = recommendResults; - helper.lastRecommendResults = recommendResults; - } - - // Subscribe to the Helper state changes for the page before widgets - // are initialized. This behavior mimics the original one of the Helper. - // It makes sense to replicate it at the `init` step. We have another - // listener on `change` below, once `init` is done. - helper.on('change', ({ isPageReset }) => { - if (isPageReset) { - resetPageFromWidgets(localWidgets); - } - }); - - derivedHelper.on('search', () => { - // The index does not manage the "staleness" of the search. This is the - // responsibility of the main instance. It does not make sense to manage - // it at the index level because it's either: all of them or none of them - // that are stalled. The queries are performed into a single network request. - instantSearchInstance.scheduleStalledRender(); - - if (__DEV__) { - checkIndexUiState({ index: this, indexUiState: localUiState }); - } - }); - - derivedHelper.on('result', ({ results }) => { - // The index does not render the results it schedules a new render - // to let all the other indices emit their own results. It allows us to - // run the render process in one pass. - instantSearchInstance.scheduleRender(); - - // the derived helper is the one which actually searches, but the helper - // which is exposed e.g. via instance.helper, doesn't search, and thus - // does not have access to lastResults, which it used to in pre-federated - // search behavior. - helper!.lastResults = results; - lastValidSearchParameters = results?._state; - }); - - // eslint-disable-next-line no-warning-comments - // TODO: listen to "result" event when events for Recommend are implemented - derivedHelper.on('recommend:result', ({ recommend }) => { - // The index does not render the results it schedules a new render - // to let all the other indices emit their own results. It allows us to - // run the render process in one pass. - instantSearchInstance.scheduleRender(); - - // the derived helper is the one which actually searches, but the helper - // which is exposed e.g. via instance.helper, doesn't search, and thus - // does not have access to lastRecommendResults. - helper!.lastRecommendResults = recommend.results; - }); - - // We compute the render state before calling `init` in a separate loop - // to construct the whole render state object that is then passed to - // `init`. - localWidgets.forEach((widget) => { - if (widget.getRenderState) { - const renderState = widget.getRenderState( - instantSearchInstance.renderState[this.getIndexId()] || {}, - createInitArgs(instantSearchInstance, this, uiState) - ); - - storeRenderState({ - renderState, - instantSearchInstance, - parent: this, - }); - } - }); - - localWidgets.forEach((widget) => { - warning( - // if it has NO getWidgetState or if it has getWidgetUiState, we don't warn - // aka we warn if there's _only_ getWidgetState - !widget.getWidgetState || Boolean(widget.getWidgetUiState), - 'The `getWidgetState` method is renamed `getWidgetUiState` and will no longer exist under that name in InstantSearch.js 5.x. Please use `getWidgetUiState` instead.' - ); - - if (widget.init) { - widget.init(createInitArgs(instantSearchInstance, this, uiState)); - } - }); - - // Subscribe to the Helper state changes for the `uiState` once widgets - // are initialized. Until the first render, state changes are part of the - // configuration step. This is mainly for backward compatibility with custom - // widgets. When the subscription happens before the `init` step, the (static) - // configuration of the widget is pushed in the URL. That's what we want to avoid. - // https://github.com/algolia/instantsearch/pull/994/commits/4a672ae3fd78809e213de0368549ef12e9dc9454 - helper.on('change', (event) => { - const { state } = event; - - const _uiState = (event as any)._uiState; - - localUiState = getLocalWidgetsUiState( - localWidgets, - { - searchParameters: state, - helper: helper!, - }, - _uiState || {} - ); - - // We don't trigger an internal change when controlled because it - // becomes the responsibility of `setUiState`. - if (!instantSearchInstance.onStateChange) { - instantSearchInstance.onInternalStateChange(); - } - }); - - if (indexInitialResults) { - // If there are initial results, we're not notified of the next results - // because we don't trigger an initial search. We therefore need to directly - // schedule a render that will render the results injected on the helper. - instantSearchInstance.scheduleRender(); - } - - if (hasRecommendWidget) { - instantSearchInstance._hasRecommendWidget = true; - } - if (hasSearchWidget) { - instantSearchInstance._hasSearchWidget = true; - } - }, - - render({ instantSearchInstance }: IndexRenderOptions) { - // we can't attach a listener to the error event of search, as the error - // then would no longer be thrown for global handlers. - if ( - instantSearchInstance.status === 'error' && - !instantSearchInstance.mainHelper!.hasPendingRequests() && - lastValidSearchParameters - ) { - helper!.setState(lastValidSearchParameters); - } - - // We only render index widgets if there are no results. - // This makes sure `render` is never called with `results` being `null`. - // If it's an isolated index without an index name, we render all widgets, - // as there are no results to display for the isolated index itself. - let widgetsToRender = - this.getResults() || - derivedHelper?.lastRecommendResults || - (isolated && !indexName) - ? localWidgets - : localWidgets.filter((widget) => widget.shouldRender); - - widgetsToRender = widgetsToRender.filter((widget) => { - if (!widget.shouldRender) { - return true; - } - - return widget.shouldRender({ instantSearchInstance }); - }); - - widgetsToRender.forEach((widget) => { - if (widget.getRenderState) { - const renderState = widget.getRenderState( - instantSearchInstance.renderState[this.getIndexId()] || {}, - createRenderArgs( - instantSearchInstance, - this, - widget - ) as RenderOptions - ); - - storeRenderState({ - renderState, - instantSearchInstance, - parent: this, - }); - } - }); - - widgetsToRender.forEach((widget) => { - // At this point, all the variables used below are set. Both `helper` - // and `derivedHelper` have been created at the `init` step. The attribute - // `lastResults` might be `null` though. It's possible that a stalled render - // happens before the result e.g with a dynamically added index the request might - // be delayed. The render is triggered for the complete tree but some parts do - // not have results yet. - - if (widget.render) { - widget.render( - createRenderArgs( - instantSearchInstance, - this, - widget - ) as RenderOptions - ); - } - }); - }, - - dispose() { - localWidgets.forEach((widget) => { - if (widget.dispose && helper) { - // The dispose function is always called once the instance is started - // (it's an effect of `removeWidgets`). The index is initialized and - // the Helper is available. We don't care about the return value of - // `dispose` because the index is removed. We can't call `removeWidgets` - // because we want to keep the widgets on the instance, to allow idempotent - // operations on `add` & `remove`. - widget.dispose({ - helper, - state: helper.state, - recommendState: helper.recommendState, - parent: this, - }); - } - }); - - localInstantSearchInstance = null; - localParent = null; - helper?.removeAllListeners(); - helper = null; - - derivedHelper?.detach(); - derivedHelper = null; - }, - - getWidgetUiState(uiState: TUiState) { - return localWidgets - .filter(isIndexWidget) - .filter((w) => !w._isolated) - .reduce( - (previousUiState, innerIndex) => - innerIndex.getWidgetUiState(previousUiState), - { - ...uiState, - [indexId]: { - ...uiState[indexId], - ...localUiState, - }, - } - ); - }, - - getWidgetState(uiState: UiState) { - warning( - false, - 'The `getWidgetState` method is renamed `getWidgetUiState` and will no longer exist under that name in InstantSearch.js 5.x. Please use `getWidgetUiState` instead.' - ); - - return this.getWidgetUiState(uiState); - }, - - getWidgetSearchParameters(searchParameters, { uiState }) { - return getLocalWidgetsSearchParameters(localWidgets, { - uiState, - initialSearchParameters: searchParameters, - }); - }, - - shouldRender() { - return true; - }, - - refreshUiState() { - localUiState = getLocalWidgetsUiState( - localWidgets, - { - searchParameters: this.getHelper()!.state, - helper: this.getHelper()!, - }, - localUiState - ); - }, - - setIndexUiState( - indexUiState: - | TIndexUiState - | ((previousIndexUiState: TIndexUiState) => TIndexUiState) - ) { - const nextIndexUiState = - typeof indexUiState === 'function' - ? indexUiState(localUiState as TIndexUiState) - : indexUiState; - - localInstantSearchInstance!.setUiState((state) => ({ - ...state, - [indexId]: nextIndexUiState, - })); - }, - }; -}; - -export default index; - -/** - * Walk up the parent chain to find the closest isolated index, or fall back to mainHelper - */ -function nearestIsolatedHelper( - current: IndexWidget | null, - mainHelper: Helper -): Helper { - while (current) { - if (current._isolated) { - return current.getHelper()!; - } - current = current.getParent(); - } - return mainHelper; -} +export { index as default } from 'instantsearch-core'; +export type * from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/widgets/infinite-hits/infinite-hits.tsx b/packages/instantsearch.js/src/widgets/infinite-hits/infinite-hits.tsx index bfe920619cd..df84ff56ada 100644 --- a/packages/instantsearch.js/src/widgets/infinite-hits/infinite-hits.tsx +++ b/packages/instantsearch.js/src/widgets/infinite-hits/infinite-hits.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import InfiniteHits from '../../components/InfiniteHits/InfiniteHits'; -import connectInfiniteHits from '../../connectors/infinite-hits/connectInfiniteHits'; +import { connectInfiniteHits } from '../../connectors'; import { withInsights } from '../../lib/insights'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; @@ -24,7 +24,7 @@ import type { InfiniteHitsRenderState, InfiniteHitsCache, InfiniteHitsWidgetDescription, -} from '../../connectors/infinite-hits/connectInfiniteHits'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { WidgetFactory, diff --git a/packages/instantsearch.js/src/widgets/looking-similar/looking-similar.tsx b/packages/instantsearch.js/src/widgets/looking-similar/looking-similar.tsx index 323522a7e69..150a4981e45 100644 --- a/packages/instantsearch.js/src/widgets/looking-similar/looking-similar.tsx +++ b/packages/instantsearch.js/src/widgets/looking-similar/looking-similar.tsx @@ -4,7 +4,7 @@ import { createLookingSimilarComponent } from 'instantsearch-ui-components'; import { Fragment, h, render } from 'preact'; import TemplateComponent from '../../components/Template/Template'; -import connectLookingSimilar from '../../connectors/looking-similar/connectLookingSimilar'; +import { connectLookingSimilar } from '../../connectors'; import { prepareTemplateProps } from '../../lib/templating'; import { getContainerNode, @@ -15,7 +15,7 @@ import type { LookingSimilarWidgetDescription, LookingSimilarConnectorParams, LookingSimilarRenderState, -} from '../../connectors/looking-similar/connectLookingSimilar'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { Template, diff --git a/packages/instantsearch.js/src/widgets/menu-select/menu-select.tsx b/packages/instantsearch.js/src/widgets/menu-select/menu-select.tsx index 39ad6a37e06..c2cd4d4e199 100644 --- a/packages/instantsearch.js/src/widgets/menu-select/menu-select.tsx +++ b/packages/instantsearch.js/src/widgets/menu-select/menu-select.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import MenuSelect from '../../components/MenuSelect/MenuSelect'; -import connectMenu from '../../connectors/menu/connectMenu'; +import { connectMenu } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -22,7 +22,7 @@ import type { MenuConnectorParams, MenuRenderState, MenuWidgetDescription, -} from '../../connectors/menu/connectMenu'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { RendererOptions, Template, WidgetFactory } from '../../types'; diff --git a/packages/instantsearch.js/src/widgets/menu/menu.tsx b/packages/instantsearch.js/src/widgets/menu/menu.tsx index 3c1103d564b..67a5fdeea63 100644 --- a/packages/instantsearch.js/src/widgets/menu/menu.tsx +++ b/packages/instantsearch.js/src/widgets/menu/menu.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import RefinementList from '../../components/RefinementList/RefinementList'; -import connectMenu from '../../connectors/menu/connectMenu'; +import { connectMenu } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -18,7 +18,7 @@ import type { MenuConnectorParams, MenuRenderState, MenuWidgetDescription, -} from '../../connectors/menu/connectMenu'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { ComponentCSSClasses, diff --git a/packages/instantsearch.js/src/widgets/numeric-menu/numeric-menu.tsx b/packages/instantsearch.js/src/widgets/numeric-menu/numeric-menu.tsx index d9a7fcd8ea7..eed3bd898c3 100644 --- a/packages/instantsearch.js/src/widgets/numeric-menu/numeric-menu.tsx +++ b/packages/instantsearch.js/src/widgets/numeric-menu/numeric-menu.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import RefinementList from '../../components/RefinementList/RefinementList'; -import connectNumericMenu from '../../connectors/numeric-menu/connectNumericMenu'; +import { connectNumericMenu } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -18,7 +18,7 @@ import type { NumericMenuConnectorParams, NumericMenuRenderState, NumericMenuWidgetDescription, -} from '../../connectors/numeric-menu/connectNumericMenu'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { ComponentCSSClasses, diff --git a/packages/instantsearch.js/src/widgets/pagination/pagination.tsx b/packages/instantsearch.js/src/widgets/pagination/pagination.tsx index 13264ac9c53..ebe00cd1719 100644 --- a/packages/instantsearch.js/src/widgets/pagination/pagination.tsx +++ b/packages/instantsearch.js/src/widgets/pagination/pagination.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import Pagination from '../../components/Pagination/Pagination'; -import connectPagination from '../../connectors/pagination/connectPagination'; +import { connectPagination } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -19,7 +19,7 @@ import type { PaginationConnectorParams, PaginationRenderState, PaginationWidgetDescription, -} from '../../connectors/pagination/connectPagination'; +} from '../../connectors'; import type { Renderer, Template, WidgetFactory } from '../../types'; const suit = component('Pagination'); diff --git a/packages/instantsearch.js/src/widgets/panel/__tests__/panel-test.ts b/packages/instantsearch.js/src/widgets/panel/__tests__/panel-test.ts index a31cff0682f..4ddcad736ca 100644 --- a/packages/instantsearch.js/src/widgets/panel/__tests__/panel-test.ts +++ b/packages/instantsearch.js/src/widgets/panel/__tests__/panel-test.ts @@ -10,7 +10,7 @@ import { createInitOptions, createRenderOptions, createDisposeOptions, -} from '../../../../test/createWidget'; +} from '../../../../../instantsearch-core/test/createWidget'; import panel from '../panel'; import type { PanelProps } from '../../../components/Panel/Panel'; diff --git a/packages/instantsearch.js/src/widgets/places/__tests__/places-test.ts b/packages/instantsearch.js/src/widgets/places/__tests__/places-test.ts index 99bcdc283a6..cacdec2d7f5 100644 --- a/packages/instantsearch.js/src/widgets/places/__tests__/places-test.ts +++ b/packages/instantsearch.js/src/widgets/places/__tests__/places-test.ts @@ -6,7 +6,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import algoliasearchHelper, { SearchParameters } from 'algoliasearch-helper'; import algoliaPlaces from 'places.js'; -import { createInitOptions } from '../../../../test/createWidget'; +import { createInitOptions } from '../../../../../instantsearch-core/test/createWidget'; import places from '../places'; import type { SearchClient } from '../../../types'; diff --git a/packages/instantsearch.js/src/widgets/powered-by/powered-by.tsx b/packages/instantsearch.js/src/widgets/powered-by/powered-by.tsx index dc5a12e7566..dd4f17fae88 100644 --- a/packages/instantsearch.js/src/widgets/powered-by/powered-by.tsx +++ b/packages/instantsearch.js/src/widgets/powered-by/powered-by.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import PoweredBy from '../../components/PoweredBy/PoweredBy'; -import connectPoweredBy from '../../connectors/powered-by/connectPoweredBy'; +import { connectPoweredBy } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -16,7 +16,7 @@ import type { PoweredByConnectorParams, PoweredByRenderState, PoweredByWidgetDescription, -} from '../../connectors/powered-by/connectPoweredBy'; +} from '../../connectors'; import type { Renderer, WidgetFactory } from '../../types'; const suit = component('PoweredBy'); diff --git a/packages/instantsearch.js/src/widgets/query-rule-context/query-rule-context.tsx b/packages/instantsearch.js/src/widgets/query-rule-context/query-rule-context.tsx index cca0d571746..6d15e365bc2 100644 --- a/packages/instantsearch.js/src/widgets/query-rule-context/query-rule-context.tsx +++ b/packages/instantsearch.js/src/widgets/query-rule-context/query-rule-context.tsx @@ -1,4 +1,4 @@ -import connectQueryRules from '../../connectors/query-rules/connectQueryRules'; +import { connectQueryRules } from '../../connectors'; import { createDocumentationMessageGenerator, noop } from '../../lib/utils'; import type { @@ -6,7 +6,7 @@ import type { ParamTransformRuleContexts, QueryRulesConnectorParams, QueryRulesWidgetDescription, -} from '../../connectors/query-rules/connectQueryRules'; +} from '../../connectors'; import type { WidgetFactory } from '../../types'; export type QueryRuleContextWidgetParams = { diff --git a/packages/instantsearch.js/src/widgets/query-rule-custom-data/__tests__/query-rule-custom-data-test.ts b/packages/instantsearch.js/src/widgets/query-rule-custom-data/__tests__/query-rule-custom-data-test.ts index 71b07a5d630..15e5baf9bde 100644 --- a/packages/instantsearch.js/src/widgets/query-rule-custom-data/__tests__/query-rule-custom-data-test.ts +++ b/packages/instantsearch.js/src/widgets/query-rule-custom-data/__tests__/query-rule-custom-data-test.ts @@ -10,7 +10,7 @@ import { render as preactRender } from 'preact'; import { createDisposeOptions, createInitOptions, -} from '../../../../test/createWidget'; +} from '../../../../../instantsearch-core/test/createWidget'; import queryRuleCustomData from '../query-rule-custom-data'; import type { QueryRuleCustomDataProps } from '../../../components/QueryRuleCustomData/QueryRuleCustomData'; diff --git a/packages/instantsearch.js/src/widgets/query-rule-custom-data/query-rule-custom-data.tsx b/packages/instantsearch.js/src/widgets/query-rule-custom-data/query-rule-custom-data.tsx index 649c84d2aab..1b3690367ae 100644 --- a/packages/instantsearch.js/src/widgets/query-rule-custom-data/query-rule-custom-data.tsx +++ b/packages/instantsearch.js/src/widgets/query-rule-custom-data/query-rule-custom-data.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import CustomData from '../../components/QueryRuleCustomData/QueryRuleCustomData'; -import connectQueryRules from '../../connectors/query-rules/connectQueryRules'; +import { connectQueryRules } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -19,7 +19,7 @@ import type { QueryRulesConnectorParams, QueryRulesRenderState, QueryRulesWidgetDescription, -} from '../../connectors/query-rules/connectQueryRules'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { WidgetFactory, Template } from '../../types'; diff --git a/packages/instantsearch.js/src/widgets/range-input/range-input.tsx b/packages/instantsearch.js/src/widgets/range-input/range-input.tsx index a95dd17708e..d958866f0f6 100644 --- a/packages/instantsearch.js/src/widgets/range-input/range-input.tsx +++ b/packages/instantsearch.js/src/widgets/range-input/range-input.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import RangeInput from '../../components/RangeInput/RangeInput'; -import connectRange from '../../connectors/range/connectRange'; +import { connectRange } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -20,7 +20,7 @@ import type { RangeConnectorParams, RangeRenderState, RangeWidgetDescription, -} from '../../connectors/range/connectRange'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { Renderer, Template, WidgetFactory } from '../../types'; diff --git a/packages/instantsearch.js/src/widgets/range-slider/__tests__/range-slider-test.ts b/packages/instantsearch.js/src/widgets/range-slider/__tests__/range-slider-test.ts index 5a657c69893..a6d79568384 100644 --- a/packages/instantsearch.js/src/widgets/range-slider/__tests__/range-slider-test.ts +++ b/packages/instantsearch.js/src/widgets/range-slider/__tests__/range-slider-test.ts @@ -13,11 +13,11 @@ import algoliasearchHelper, { } from 'algoliasearch-helper'; import { render as preactRender } from 'preact'; -import { createInstantSearch } from '../../../../test/createInstantSearch'; +import { createInstantSearch } from '../../../../../instantsearch-core/test/createInstantSearch'; import { createInitOptions, createRenderOptions, -} from '../../../../test/createWidget'; +} from '../../../../../instantsearch-core/test/createWidget'; import rangeSlider from '../range-slider'; import type { InstantSearch } from '../../../types'; diff --git a/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx b/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx index dcad777b7aa..fa8569f2ffe 100644 --- a/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx +++ b/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import Slider from '../../components/Slider/Slider'; -import connectRange from '../../connectors/range/connectRange'; +import { connectRange } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -17,7 +17,7 @@ import type { RangeConnectorParams, RangeRenderState, RangeWidgetDescription, -} from '../../connectors/range/connectRange'; +} from '../../connectors'; import type { Renderer, WidgetFactory } from '../../types'; const withUsage = createDocumentationMessageGenerator({ name: 'range-slider' }); diff --git a/packages/instantsearch.js/src/widgets/rating-menu/rating-menu.tsx b/packages/instantsearch.js/src/widgets/rating-menu/rating-menu.tsx index 8d18e2c96e6..d29f0519112 100644 --- a/packages/instantsearch.js/src/widgets/rating-menu/rating-menu.tsx +++ b/packages/instantsearch.js/src/widgets/rating-menu/rating-menu.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import RefinementList from '../../components/RefinementList/RefinementList'; -import connectRatingMenu from '../../connectors/rating-menu/connectRatingMenu'; +import { connectRatingMenu } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -18,7 +18,7 @@ import type { RatingMenuWidgetDescription, RatingMenuConnectorParams, RatingMenuRenderState, -} from '../../connectors/rating-menu/connectRatingMenu'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { ComponentCSSClasses, diff --git a/packages/instantsearch.js/src/widgets/refinement-list/refinement-list.tsx b/packages/instantsearch.js/src/widgets/refinement-list/refinement-list.tsx index bdccb34a9f4..480d9c4c629 100644 --- a/packages/instantsearch.js/src/widgets/refinement-list/refinement-list.tsx +++ b/packages/instantsearch.js/src/widgets/refinement-list/refinement-list.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import RefinementList from '../../components/RefinementList/RefinementList'; -import connectRefinementList from '../../connectors/refinement-list/connectRefinementList'; +import { connectRefinementList } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -21,7 +21,7 @@ import type { RefinementListRenderState, RefinementListConnectorParams, RefinementListWidgetDescription, -} from '../../connectors/refinement-list/connectRefinementList'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { Template, WidgetFactory, Renderer } from '../../types'; import type { SearchBoxTemplates } from '../search-box/search-box'; diff --git a/packages/instantsearch.js/src/widgets/related-products/related-products.tsx b/packages/instantsearch.js/src/widgets/related-products/related-products.tsx index 1c821716321..4fad01a3eec 100644 --- a/packages/instantsearch.js/src/widgets/related-products/related-products.tsx +++ b/packages/instantsearch.js/src/widgets/related-products/related-products.tsx @@ -4,7 +4,7 @@ import { createRelatedProductsComponent } from 'instantsearch-ui-components'; import { Fragment, h, render } from 'preact'; import TemplateComponent from '../../components/Template/Template'; -import connectRelatedProducts from '../../connectors/related-products/connectRelatedProducts'; +import { connectRelatedProducts } from '../../connectors'; import { prepareTemplateProps } from '../../lib/templating'; import { getContainerNode, @@ -15,7 +15,7 @@ import type { RelatedProductsWidgetDescription, RelatedProductsConnectorParams, RelatedProductsRenderState, -} from '../../connectors/related-products/connectRelatedProducts'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { Template, diff --git a/packages/instantsearch.js/src/widgets/relevant-sort/__tests__/relevant-sort-test.ts b/packages/instantsearch.js/src/widgets/relevant-sort/__tests__/relevant-sort-test.ts index 3c73ccd9661..4af0efa07da 100644 --- a/packages/instantsearch.js/src/widgets/relevant-sort/__tests__/relevant-sort-test.ts +++ b/packages/instantsearch.js/src/widgets/relevant-sort/__tests__/relevant-sort-test.ts @@ -12,7 +12,7 @@ import { render } from 'preact'; import { createInitOptions, createRenderOptions, -} from '../../../../test/createWidget'; +} from '../../../../../instantsearch-core/test/createWidget'; import relevantSort from '../relevant-sort'; import type { RelevantSortTemplates } from '../relevant-sort'; diff --git a/packages/instantsearch.js/src/widgets/relevant-sort/relevant-sort.tsx b/packages/instantsearch.js/src/widgets/relevant-sort/relevant-sort.tsx index 870c5ee28e1..7fa9313bbb8 100644 --- a/packages/instantsearch.js/src/widgets/relevant-sort/relevant-sort.tsx +++ b/packages/instantsearch.js/src/widgets/relevant-sort/relevant-sort.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import RelevantSort from '../../components/RelevantSort/RelevantSort'; -import connectRelevantSort from '../../connectors/relevant-sort/connectRelevantSort'; +import { connectRelevantSort } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -21,7 +21,7 @@ import type { RelevantSortConnectorParams, RelevantSortRenderState, RelevantSortWidgetDescription, -} from '../../connectors/relevant-sort/connectRelevantSort'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { WidgetFactory, Template } from '../../types'; diff --git a/packages/instantsearch.js/src/widgets/search-box/search-box.tsx b/packages/instantsearch.js/src/widgets/search-box/search-box.tsx index ab361559e26..7b30af20b09 100644 --- a/packages/instantsearch.js/src/widgets/search-box/search-box.tsx +++ b/packages/instantsearch.js/src/widgets/search-box/search-box.tsx @@ -1,11 +1,11 @@ /** @jsx h */ +import { isChatBusy, openChat } from 'instantsearch-core'; import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import SearchBox from '../../components/SearchBox/SearchBox'; -import connectSearchBox from '../../connectors/search-box/connectSearchBox'; -import { isChatBusy, openChat } from '../../lib/chat'; +import { connectSearchBox } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -18,13 +18,13 @@ import type { SearchBoxComponentCSSClasses, SearchBoxComponentTemplates, } from '../../components/SearchBox/SearchBox'; -import type { ChatRenderState } from '../../connectors/chat/connectChat'; import type { SearchBoxConnectorParams, SearchBoxRenderState, SearchBoxWidgetDescription, -} from '../../connectors/search-box/connectSearchBox'; +} from '../../connectors'; import type { WidgetFactory, Template, RendererOptions } from '../../types'; +import type { ChatRenderState } from 'instantsearch-core'; const withUsage = createDocumentationMessageGenerator({ name: 'search-box' }); const suit = component('SearchBox'); diff --git a/packages/instantsearch.js/src/widgets/sort-by/sort-by.tsx b/packages/instantsearch.js/src/widgets/sort-by/sort-by.tsx index 0b3a1c96e8d..590b1187a2f 100644 --- a/packages/instantsearch.js/src/widgets/sort-by/sort-by.tsx +++ b/packages/instantsearch.js/src/widgets/sort-by/sort-by.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import Selector from '../../components/Selector/Selector'; -import connectSortBy from '../../connectors/sort-by/connectSortBy'; +import { connectSortBy } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -17,7 +17,7 @@ import type { SortByItem, SortByRenderState, SortByWidgetDescription, -} from '../../connectors/sort-by/connectSortBy'; +} from '../../connectors'; import type { Renderer, TransformItems, WidgetFactory } from '../../types'; const withUsage = createDocumentationMessageGenerator({ name: 'sort-by' }); diff --git a/packages/instantsearch.js/src/widgets/stats/stats.tsx b/packages/instantsearch.js/src/widgets/stats/stats.tsx index 9978bf8ccd6..61dd87c8f1d 100644 --- a/packages/instantsearch.js/src/widgets/stats/stats.tsx +++ b/packages/instantsearch.js/src/widgets/stats/stats.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import Stats from '../../components/Stats/Stats'; -import connectStats from '../../connectors/stats/connectStats'; +import { connectStats } from '../../connectors'; import { formatNumber } from '../../lib/formatNumber'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; @@ -21,7 +21,7 @@ import type { StatsConnectorParams, StatsRenderState, StatsWidgetDescription, -} from '../../connectors/stats/connectStats'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { Renderer, Template, WidgetFactory } from '../../types'; diff --git a/packages/instantsearch.js/src/widgets/toggle-refinement/toggle-refinement.tsx b/packages/instantsearch.js/src/widgets/toggle-refinement/toggle-refinement.tsx index 1a4f7f3e93e..d14046fe0ec 100644 --- a/packages/instantsearch.js/src/widgets/toggle-refinement/toggle-refinement.tsx +++ b/packages/instantsearch.js/src/widgets/toggle-refinement/toggle-refinement.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import ToggleRefinement from '../../components/ToggleRefinement/ToggleRefinement'; -import connectToggleRefinement from '../../connectors/toggle-refinement/connectToggleRefinement'; +import { connectToggleRefinement } from '../../connectors'; import { component } from '../../lib/suit'; import { prepareTemplateProps } from '../../lib/templating'; import { @@ -22,7 +22,7 @@ import type { ToggleRefinementConnectorParams, ToggleRefinementWidgetDescription, ToggleRefinementRenderState, -} from '../../connectors/toggle-refinement/connectToggleRefinement'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { RendererOptions, Template, WidgetFactory } from '../../types'; diff --git a/packages/instantsearch.js/src/widgets/trending-facets/trending-facets.tsx b/packages/instantsearch.js/src/widgets/trending-facets/trending-facets.tsx index 4a6724405fc..bb4145d13f8 100644 --- a/packages/instantsearch.js/src/widgets/trending-facets/trending-facets.tsx +++ b/packages/instantsearch.js/src/widgets/trending-facets/trending-facets.tsx @@ -4,7 +4,7 @@ import { createTrendingFacetsComponent } from 'instantsearch-ui-components'; import { Fragment, h, render } from 'preact'; import TemplateComponent from '../../components/Template/Template'; -import connectTrendingFacets from '../../connectors/trending-facets/connectTrendingFacets'; +import { connectTrendingFacets } from '../../connectors'; import { prepareTemplateProps } from '../../lib/templating'; import { getContainerNode, @@ -15,7 +15,7 @@ import type { TrendingFacetsWidgetDescription, TrendingFacetsConnectorParams, TrendingFacetsRenderState, -} from '../../connectors/trending-facets/connectTrendingFacets'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { Template, @@ -23,7 +23,7 @@ import type { Renderer, RecommendResponse, } from '../../types'; -import type { TrendingFacetItem } from '../../types/recommend'; +import type { TrendingFacetItem } from 'instantsearch-core'; import type { TrendingFacetsClassNames, TrendingFacetsProps as TrendingFacetsUiProps, diff --git a/packages/instantsearch.js/src/widgets/trending-items/trending-items.tsx b/packages/instantsearch.js/src/widgets/trending-items/trending-items.tsx index e6df0cac86b..e2dfd8aeb05 100644 --- a/packages/instantsearch.js/src/widgets/trending-items/trending-items.tsx +++ b/packages/instantsearch.js/src/widgets/trending-items/trending-items.tsx @@ -4,7 +4,7 @@ import { createTrendingItemsComponent } from 'instantsearch-ui-components'; import { Fragment, h, render } from 'preact'; import TemplateComponent from '../../components/Template/Template'; -import connectTrendingItems from '../../connectors/trending-items/connectTrendingItems'; +import { connectTrendingItems } from '../../connectors'; import { prepareTemplateProps } from '../../lib/templating'; import { getContainerNode, @@ -15,7 +15,7 @@ import type { TrendingItemsWidgetDescription, TrendingItemsConnectorParams, TrendingItemsRenderState, -} from '../../connectors/trending-items/connectTrendingItems'; +} from '../../connectors'; import type { PreparedTemplateProps } from '../../lib/templating'; import type { Template, diff --git a/packages/instantsearch.js/src/widgets/voice-search/__tests__/voice-search-test.ts b/packages/instantsearch.js/src/widgets/voice-search/__tests__/voice-search-test.ts index 607b036f436..cb03786105f 100644 --- a/packages/instantsearch.js/src/widgets/voice-search/__tests__/voice-search-test.ts +++ b/packages/instantsearch.js/src/widgets/voice-search/__tests__/voice-search-test.ts @@ -16,14 +16,14 @@ import { render as preactRender } from 'preact'; import { createInitOptions, createRenderOptions, -} from '../../../../test/createWidget'; +} from '../../../../../instantsearch-core/test/createWidget'; import voiceSearch from '../voice-search'; import type { VoiceSearchProps } from '../../../components/VoiceSearch/VoiceSearch'; -import type { VoiceSearchHelper } from '../../../lib/voiceSearchHelper/types'; import type { Widget } from '../../../types'; import type { VoiceSearchWidgetParams } from '../voice-search'; import type { AlgoliaSearchHelper as Helper } from 'algoliasearch-helper'; +import type { VoiceSearchHelper } from 'instantsearch-core'; import type { VNode } from 'preact'; const render = castToJestMock(preactRender); diff --git a/packages/instantsearch.js/src/widgets/voice-search/voice-search.tsx b/packages/instantsearch.js/src/widgets/voice-search/voice-search.tsx index 469c8835a23..bddbb98d9f3 100644 --- a/packages/instantsearch.js/src/widgets/voice-search/voice-search.tsx +++ b/packages/instantsearch.js/src/widgets/voice-search/voice-search.tsx @@ -4,7 +4,7 @@ import { cx } from 'instantsearch-ui-components'; import { h, render } from 'preact'; import VoiceSearchComponent from '../../components/VoiceSearch/VoiceSearch'; -import connectVoiceSearch from '../../connectors/voice-search/connectVoiceSearch'; +import { connectVoiceSearch } from '../../connectors'; import { component } from '../../lib/suit'; import { getContainerNode, @@ -21,10 +21,10 @@ import type { VoiceSearchConnectorParams, VoiceSearchRenderState, VoiceSearchWidgetDescription, -} from '../../connectors/voice-search/connectVoiceSearch'; -import type { CreateVoiceSearchHelper } from '../../lib/voiceSearchHelper/types'; +} from '../../connectors'; import type { WidgetFactory, Template, Renderer } from '../../types'; import type { PlainSearchParameters } from 'algoliasearch-helper'; +import type { CreateVoiceSearchHelper } from 'instantsearch-core'; const withUsage = createDocumentationMessageGenerator({ name: 'voice-search' }); const suit = component('VoiceSearch'); diff --git a/packages/instantsearch.js/stories/breadcrumb.stories.ts b/packages/instantsearch.js/stories/breadcrumb.stories.ts index da4408a697a..a57e5ccd147 100644 --- a/packages/instantsearch.js/stories/breadcrumb.stories.ts +++ b/packages/instantsearch.js/stories/breadcrumb.stories.ts @@ -1,7 +1,7 @@ import { storiesOf } from '@storybook/html'; import { withHits, withLifecycle } from '../.storybook/decorators'; -import connectHierarchicalMenu from '../src/connectors/hierarchical-menu/connectHierarchicalMenu'; +import { connectHierarchicalMenu } from '../src/connectors'; import { noop } from '../src/lib/utils'; const virtualHierarchicalMenu = (args = {}) => diff --git a/packages/instantsearch.js/stories/panel.stories.ts b/packages/instantsearch.js/stories/panel.stories.ts index 296a648da9a..068b86206c2 100644 --- a/packages/instantsearch.js/stories/panel.stories.ts +++ b/packages/instantsearch.js/stories/panel.stories.ts @@ -1,7 +1,7 @@ import { storiesOf } from '@storybook/html'; import { withHits } from '../.storybook/decorators'; -import connectHierarchicalMenu from '../src/connectors/hierarchical-menu/connectHierarchicalMenu'; +import { connectHierarchicalMenu } from '../src/connectors'; import { noop } from '../src/lib/utils'; const virtualHierarchicalMenu = (args = {}) => diff --git a/packages/react-instantsearch-core/package.json b/packages/react-instantsearch-core/package.json index b79d4f8d68d..aed033b1791 100644 --- a/packages/react-instantsearch-core/package.json +++ b/packages/react-instantsearch-core/package.json @@ -47,8 +47,8 @@ "dependencies": { "@swc/helpers": "0.5.18", "algoliasearch-helper": "3.29.1", - "instantsearch.js": "4.103.0", - "use-sync-external-store": "^1.0.0" + "use-sync-external-store": "^1.0.0", + "instantsearch-core": "0.1.0" }, "devDependencies": { "@types/use-sync-external-store": "0.0.3", diff --git a/packages/react-instantsearch-core/src/components/DynamicWidgets.tsx b/packages/react-instantsearch-core/src/components/DynamicWidgets.tsx index f49a5a5c315..a9b5d80d7d5 100644 --- a/packages/react-instantsearch-core/src/components/DynamicWidgets.tsx +++ b/packages/react-instantsearch-core/src/components/DynamicWidgets.tsx @@ -4,7 +4,7 @@ import { useDynamicWidgets } from '../connectors/useDynamicWidgets'; import { invariant } from '../lib/invariant'; import { warn } from '../lib/warn'; -import type { DynamicWidgetsConnectorParams } from 'instantsearch.js/es/connectors/dynamic-widgets/connectDynamicWidgets'; +import type { DynamicWidgetsConnectorParams } from 'instantsearch-core'; import type { ReactElement, ComponentType, ReactNode } from 'react'; function DefaultFallbackComponent() { diff --git a/packages/react-instantsearch-core/src/components/Feeds.tsx b/packages/react-instantsearch-core/src/components/Feeds.tsx index de8e6dae548..44fa6f363bc 100644 --- a/packages/react-instantsearch-core/src/components/Feeds.tsx +++ b/packages/react-instantsearch-core/src/components/Feeds.tsx @@ -1,4 +1,4 @@ -import { createFeedContainer } from 'instantsearch.js/es/connectors/feeds/FeedContainer'; +import { createFeedContainer } from 'instantsearch-core'; import React, { useEffect, useRef } from 'react'; import { useFeeds } from '../connectors/useFeeds'; @@ -6,8 +6,8 @@ import { IndexContext } from '../lib/IndexContext'; import { useIndexContext } from '../lib/useIndexContext'; import { useInstantSearchContext } from '../lib/useInstantSearchContext'; -import type { FeedsConnectorParams } from 'instantsearch.js/es/connectors/feeds/connectFeeds'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { FeedsConnectorParams } from 'instantsearch-core'; +import type { IndexWidget } from 'instantsearch-core'; import type { ReactNode } from 'react'; export type FeedsProps = FeedsConnectorParams & { diff --git a/packages/react-instantsearch-core/src/components/InstantSearch.tsx b/packages/react-instantsearch-core/src/components/InstantSearch.tsx index 0cfcf190a42..e59c417cd2b 100644 --- a/packages/react-instantsearch-core/src/components/InstantSearch.tsx +++ b/packages/react-instantsearch-core/src/components/InstantSearch.tsx @@ -11,7 +11,7 @@ import type { import type { InstantSearch as InstantSearchType, UiState, -} from 'instantsearch.js'; +} from 'instantsearch-core'; export type InstantSearchProps< TUiState extends UiState = UiState, diff --git a/packages/react-instantsearch-core/src/components/InstantSearchSSRProvider.tsx b/packages/react-instantsearch-core/src/components/InstantSearchSSRProvider.tsx index a179ce9860e..13adddaad91 100644 --- a/packages/react-instantsearch-core/src/components/InstantSearchSSRProvider.tsx +++ b/packages/react-instantsearch-core/src/components/InstantSearchSSRProvider.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { InstantSearchSSRContext } from '../lib/InstantSearchSSRContext'; import type { InternalInstantSearch } from '../lib/useInstantSearchApi'; -import type { InitialResults, UiState } from 'instantsearch.js'; +import type { InitialResults, UiState } from 'instantsearch-core'; import type { ReactNode } from 'react'; export type InstantSearchServerState = { diff --git a/packages/react-instantsearch-core/src/components/InstantSearchServerContext.ts b/packages/react-instantsearch-core/src/components/InstantSearchServerContext.ts index 3f7d46a03fb..5df00c85bc9 100644 --- a/packages/react-instantsearch-core/src/components/InstantSearchServerContext.ts +++ b/packages/react-instantsearch-core/src/components/InstantSearchServerContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import type { InstantSearch, UiState } from 'instantsearch.js'; +import type { InstantSearch, UiState } from 'instantsearch-core'; export type InstantSearchServerContextApi< TUiState extends UiState, diff --git a/packages/react-instantsearch-core/src/components/__tests__/DynamicWidgets.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/DynamicWidgets.test.tsx index 91ab2d8aedd..d1ea5b524fd 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/DynamicWidgets.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/DynamicWidgets.test.tsx @@ -21,7 +21,7 @@ import type { UseMenuProps } from '../../connectors/useMenu'; import type { UsePaginationProps } from '../../connectors/usePagination'; import type { UseRefinementListProps } from '../../connectors/useRefinementList'; import type { InstantSearchProps } from '../InstantSearch'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { IndexWidget } from 'instantsearch-core'; expect.addSnapshotSerializer(widgetSnapshotSerializer); diff --git a/packages/react-instantsearch-core/src/components/__tests__/Feeds.integration.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/Feeds.integration.test.tsx index 1b859bac998..726352c6b63 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/Feeds.integration.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/Feeds.integration.test.tsx @@ -13,7 +13,7 @@ import { InstantSearch } from '../InstantSearch'; import { InstantSearchSSRProvider } from '../InstantSearchSSRProvider'; import type { InstantSearchProps } from '../InstantSearch'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { IndexWidget } from 'instantsearch-core'; function createInstantSearchMock() { const indexContextRef = createRef(); diff --git a/packages/react-instantsearch-core/src/components/__tests__/Feeds.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/Feeds.test.tsx index c541c5996ab..c667411b9f1 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/Feeds.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/Feeds.test.tsx @@ -3,7 +3,7 @@ */ import { act, render, screen, waitFor } from '@testing-library/react'; -import { createFeedContainer } from 'instantsearch.js/es/connectors/feeds/FeedContainer'; +import { createFeedContainer } from 'instantsearch-core'; import React from 'react'; import { IndexContext } from '../../lib/IndexContext'; @@ -11,7 +11,7 @@ import { InstantSearchContext } from '../../lib/InstantSearchContext'; import { useIndexContext } from '../../lib/useIndexContext'; import { Feeds } from '../Feeds'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { IndexWidget } from 'instantsearch-core'; let mockFeedIDs: string[] = []; @@ -19,7 +19,7 @@ jest.mock('../../connectors/useFeeds', () => ({ useFeeds: jest.fn(() => ({ feedIDs: mockFeedIDs })), })); -jest.mock('instantsearch.js/es/connectors/feeds/FeedContainer', () => ({ +jest.mock('instantsearch-core', () => ({ createFeedContainer: jest.fn(), })); diff --git a/packages/react-instantsearch-core/src/components/__tests__/Index.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/Index.test.tsx index d6262785b00..8da30c3e98b 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/Index.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/Index.test.tsx @@ -17,7 +17,7 @@ import { Index } from '../Index'; import { InstantSearch } from '../InstantSearch'; import { InstantSearchSSRProvider } from '../InstantSearchSSRProvider'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { IndexWidget } from 'instantsearch-core'; describe('Index', () => { test('throws when used outside of ', () => { diff --git a/packages/react-instantsearch-core/src/components/__tests__/InstantSearch.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/InstantSearch.test.tsx index f8abe3ac951..873f6623a2f 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/InstantSearch.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/InstantSearch.test.tsx @@ -6,8 +6,8 @@ import { createAlgoliaSearchClient } from '@instantsearch/mocks'; import { createInstantSearchSpy, wait } from '@instantsearch/testutils'; import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { history } from 'instantsearch.js/es/lib/routers'; -import { simple } from 'instantsearch.js/es/lib/stateMappings'; +import { history } from 'instantsearch-core'; +import { simple } from 'instantsearch-core'; import React, { StrictMode, Suspense, version as ReactVersion } from 'react'; import { SearchBox } from 'react-instantsearch'; diff --git a/packages/react-instantsearch-core/src/components/__tests__/InstantSearchSSRProvider.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/InstantSearchSSRProvider.test.tsx index 8089591325f..7d403d8f36c 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/InstantSearchSSRProvider.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/InstantSearchSSRProvider.test.tsx @@ -11,15 +11,15 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import algoliasearchV4 from 'algoliasearch-v4'; import { algoliasearch as algoliasearchV5 } from 'algoliasearch-v5'; -import { history } from 'instantsearch.js/es/lib/routers'; -import { simple } from 'instantsearch.js/es/lib/stateMappings'; +import { history } from 'instantsearch-core'; +import { simple } from 'instantsearch-core'; import React, { StrictMode } from 'react'; import { Hits, RefinementList, SearchBox } from 'react-instantsearch'; import { InstantSearch } from '../InstantSearch'; import { InstantSearchSSRProvider } from '../InstantSearchSSRProvider'; -import type { Hit as AlgoliaHit, SearchClient } from 'instantsearch.js'; +import type { Hit as AlgoliaHit, SearchClient } from 'instantsearch-core'; function HitComponent({ hit }: { hit: AlgoliaHit }) { return <>{hit.objectID}; diff --git a/packages/react-instantsearch-core/src/components/__tests__/routing/dispose-start.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/routing/dispose-start.test.tsx index 806280a5872..29e44100281 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/routing/dispose-start.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/routing/dispose-start.test.tsx @@ -4,7 +4,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { render, waitFor } from '@testing-library/react'; -import historyRouter from 'instantsearch.js/es/lib/routers/history'; +import { history as historyRouter } from 'instantsearch-core'; import React, { useEffect } from 'react'; import { InstantSearch, SearchBox, useSearchBox } from 'react-instantsearch'; diff --git a/packages/react-instantsearch-core/src/components/__tests__/routing/external-influence.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/routing/external-influence.test.tsx index b26bc801871..b9d631db14e 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/routing/external-influence.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/routing/external-influence.test.tsx @@ -4,7 +4,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { render, waitFor } from '@testing-library/react'; -import historyRouter from 'instantsearch.js/es/lib/routers/history'; +import { history as historyRouter } from 'instantsearch-core'; import React, { useEffect } from 'react'; import { InstantSearch, SearchBox, useSearchBox } from 'react-instantsearch'; diff --git a/packages/react-instantsearch-core/src/components/__tests__/routing/modal.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/routing/modal.test.tsx index 0ba66edce8a..82b607200dd 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/routing/modal.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/routing/modal.test.tsx @@ -5,7 +5,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import historyRouter from 'instantsearch.js/es/lib/routers/history'; +import { history as historyRouter } from 'instantsearch-core'; import React from 'react'; import { InstantSearch, SearchBox } from 'react-instantsearch'; diff --git a/packages/react-instantsearch-core/src/components/__tests__/routing/spa-debounced.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/routing/spa-debounced.test.tsx index dd1676edea2..8aa938690ca 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/routing/spa-debounced.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/routing/spa-debounced.test.tsx @@ -5,7 +5,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import historyRouter from 'instantsearch.js/es/lib/routers/history'; +import { history as historyRouter } from 'instantsearch-core'; import React from 'react'; import { InstantSearch, SearchBox } from 'react-instantsearch'; diff --git a/packages/react-instantsearch-core/src/components/__tests__/routing/spa-replace-state.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/routing/spa-replace-state.test.tsx index a17599b7aba..8bc8ef68ec7 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/routing/spa-replace-state.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/routing/spa-replace-state.test.tsx @@ -5,7 +5,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import historyRouter from 'instantsearch.js/es/lib/routers/history'; +import { history as historyRouter } from 'instantsearch-core'; import React from 'react'; import { InstantSearch, SearchBox } from 'react-instantsearch'; diff --git a/packages/react-instantsearch-core/src/components/__tests__/routing/spa.test.tsx b/packages/react-instantsearch-core/src/components/__tests__/routing/spa.test.tsx index 67a589db950..062afe52bee 100644 --- a/packages/react-instantsearch-core/src/components/__tests__/routing/spa.test.tsx +++ b/packages/react-instantsearch-core/src/components/__tests__/routing/spa.test.tsx @@ -5,7 +5,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import historyRouter from 'instantsearch.js/es/lib/routers/history'; +import { history as historyRouter } from 'instantsearch-core'; import React from 'react'; import { InstantSearch, SearchBox } from 'react-instantsearch'; diff --git a/packages/react-instantsearch-core/src/connectors/useAutocomplete.ts b/packages/react-instantsearch-core/src/connectors/useAutocomplete.ts index 6b8d54b1774..596bb37c471 100644 --- a/packages/react-instantsearch-core/src/connectors/useAutocomplete.ts +++ b/packages/react-instantsearch-core/src/connectors/useAutocomplete.ts @@ -1,4 +1,4 @@ -import connectAutocomplete from 'instantsearch.js/es/connectors/autocomplete/connectAutocomplete'; +import { connectAutocomplete as connectAutocomplete } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { AutocompleteConnectorParams, AutocompleteWidgetDescription, -} from 'instantsearch.js/es/connectors/autocomplete/connectAutocomplete'; +} from 'instantsearch-core'; export type UseAutocompleteProps = AutocompleteConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useBreadcrumb.ts b/packages/react-instantsearch-core/src/connectors/useBreadcrumb.ts index 9e61d459a7f..1a0c7ad1f8c 100644 --- a/packages/react-instantsearch-core/src/connectors/useBreadcrumb.ts +++ b/packages/react-instantsearch-core/src/connectors/useBreadcrumb.ts @@ -1,4 +1,4 @@ -import connectBreadcrumb from 'instantsearch.js/es/connectors/breadcrumb/connectBreadcrumb'; +import { connectBreadcrumb as connectBreadcrumb } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { BreadcrumbConnectorParams, BreadcrumbWidgetDescription, -} from 'instantsearch.js/es/connectors/breadcrumb/connectBreadcrumb'; +} from 'instantsearch-core'; export type UseBreadcrumbProps = BreadcrumbConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useChat.ts b/packages/react-instantsearch-core/src/connectors/useChat.ts index 973161e0739..50d5ae4fcd3 100644 --- a/packages/react-instantsearch-core/src/connectors/useChat.ts +++ b/packages/react-instantsearch-core/src/connectors/useChat.ts @@ -1,4 +1,4 @@ -import connectChat from 'instantsearch.js/es/connectors/chat/connectChat'; +import { connectChat as connectChat } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -7,8 +7,8 @@ import type { ChatConnector, ChatConnectorParams, ChatWidgetDescription, -} from 'instantsearch.js/es/connectors/chat/connectChat'; -import type { UIMessage } from 'instantsearch.js/es/lib/chat'; +} from 'instantsearch-core'; +import type { UIMessage } from 'instantsearch-core'; export type UseChatProps = ChatConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useChatTrigger.ts b/packages/react-instantsearch-core/src/connectors/useChatTrigger.ts index 1338a9fb07e..e76db6fb4c2 100644 --- a/packages/react-instantsearch-core/src/connectors/useChatTrigger.ts +++ b/packages/react-instantsearch-core/src/connectors/useChatTrigger.ts @@ -1,4 +1,4 @@ -import connectChatTrigger from 'instantsearch.js/es/connectors/chat/connectChatTrigger'; +import { connectChatTrigger } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { ChatTriggerConnectorParams, ChatTriggerWidgetDescription, -} from 'instantsearch.js/es/connectors/chat/connectChatTrigger'; +} from 'instantsearch-core'; export type UseChatTriggerProps = ChatTriggerConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useClearRefinements.ts b/packages/react-instantsearch-core/src/connectors/useClearRefinements.ts index 4c71e8ec4ce..030eeac7fd7 100644 --- a/packages/react-instantsearch-core/src/connectors/useClearRefinements.ts +++ b/packages/react-instantsearch-core/src/connectors/useClearRefinements.ts @@ -1,4 +1,4 @@ -import connectClearRefinements from 'instantsearch.js/es/connectors/clear-refinements/connectClearRefinements'; +import { connectClearRefinements as connectClearRefinements } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { ClearRefinementsConnectorParams, ClearRefinementsWidgetDescription, -} from 'instantsearch.js/es/connectors/clear-refinements/connectClearRefinements'; +} from 'instantsearch-core'; export type UseClearRefinementsProps = ClearRefinementsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useConfigure.ts b/packages/react-instantsearch-core/src/connectors/useConfigure.ts index 20306b05df8..b4e4bce40ab 100644 --- a/packages/react-instantsearch-core/src/connectors/useConfigure.ts +++ b/packages/react-instantsearch-core/src/connectors/useConfigure.ts @@ -1,4 +1,4 @@ -import connectConfigure from 'instantsearch.js/es/connectors/configure/connectConfigure'; +import { connectConfigure as connectConfigure } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { ConfigureConnectorParams, ConfigureWidgetDescription, -} from 'instantsearch.js/es/connectors/configure/connectConfigure'; +} from 'instantsearch-core'; export type UseConfigureProps = ConfigureConnectorParams['searchParameters']; diff --git a/packages/react-instantsearch-core/src/connectors/useCurrentRefinements.ts b/packages/react-instantsearch-core/src/connectors/useCurrentRefinements.ts index 02f3765d573..75d2825eabb 100644 --- a/packages/react-instantsearch-core/src/connectors/useCurrentRefinements.ts +++ b/packages/react-instantsearch-core/src/connectors/useCurrentRefinements.ts @@ -1,4 +1,4 @@ -import connectCurrentRefinements from 'instantsearch.js/es/connectors/current-refinements/connectCurrentRefinements'; +import { connectCurrentRefinements as connectCurrentRefinements } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { CurrentRefinementsConnectorParams, CurrentRefinementsWidgetDescription, -} from 'instantsearch.js/es/connectors/current-refinements/connectCurrentRefinements'; +} from 'instantsearch-core'; export type UseCurrentRefinementsProps = CurrentRefinementsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useDynamicWidgets.ts b/packages/react-instantsearch-core/src/connectors/useDynamicWidgets.ts index 6735c048b38..f034390a176 100644 --- a/packages/react-instantsearch-core/src/connectors/useDynamicWidgets.ts +++ b/packages/react-instantsearch-core/src/connectors/useDynamicWidgets.ts @@ -1,4 +1,4 @@ -import connectDynamicWidgets from 'instantsearch.js/es/connectors/dynamic-widgets/connectDynamicWidgets'; +import { connectDynamicWidgets as connectDynamicWidgets } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { DynamicWidgetsConnectorParams, DynamicWidgetsWidgetDescription, -} from 'instantsearch.js/es/connectors/dynamic-widgets/connectDynamicWidgets'; +} from 'instantsearch-core'; export type UseDynamicWidgetsProps = Omit< DynamicWidgetsConnectorParams, diff --git a/packages/react-instantsearch-core/src/connectors/useFeeds.ts b/packages/react-instantsearch-core/src/connectors/useFeeds.ts index 24db4dd9131..da43ac0eba3 100644 --- a/packages/react-instantsearch-core/src/connectors/useFeeds.ts +++ b/packages/react-instantsearch-core/src/connectors/useFeeds.ts @@ -1,4 +1,4 @@ -import connectFeeds from 'instantsearch.js/es/connectors/feeds/connectFeeds'; +import { connectFeeds as connectFeeds } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { FeedsConnectorParams, FeedsWidgetDescription, -} from 'instantsearch.js/es/connectors/feeds/connectFeeds'; +} from 'instantsearch-core'; export type UseFeedsProps = FeedsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useFilterSuggestions.ts b/packages/react-instantsearch-core/src/connectors/useFilterSuggestions.ts index 34e04dce2e1..8986a14e397 100644 --- a/packages/react-instantsearch-core/src/connectors/useFilterSuggestions.ts +++ b/packages/react-instantsearch-core/src/connectors/useFilterSuggestions.ts @@ -1,4 +1,4 @@ -import connectFilterSuggestions from 'instantsearch.js/es/connectors/filter-suggestions/connectFilterSuggestions'; +import { connectFilterSuggestions as connectFilterSuggestions } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { FilterSuggestionsConnectorParams, FilterSuggestionsWidgetDescription, -} from 'instantsearch.js/es/connectors/filter-suggestions/connectFilterSuggestions'; +} from 'instantsearch-core'; export type UseFilterSuggestionsProps = FilterSuggestionsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useFrequentlyBoughtTogether.ts b/packages/react-instantsearch-core/src/connectors/useFrequentlyBoughtTogether.ts index 34a95accc1d..1baa957d329 100644 --- a/packages/react-instantsearch-core/src/connectors/useFrequentlyBoughtTogether.ts +++ b/packages/react-instantsearch-core/src/connectors/useFrequentlyBoughtTogether.ts @@ -1,14 +1,14 @@ -import connectFrequentlyBoughtTogether from 'instantsearch.js/es/connectors/frequently-bought-together/connectFrequentlyBoughtTogether'; +import { connectFrequentlyBoughtTogether as connectFrequentlyBoughtTogether } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { BaseHit } from 'instantsearch.js'; +import type { BaseHit } from 'instantsearch-core'; import type { FrequentlyBoughtTogetherConnector, FrequentlyBoughtTogetherConnectorParams, FrequentlyBoughtTogetherWidgetDescription, -} from 'instantsearch.js/es/connectors/frequently-bought-together/connectFrequentlyBoughtTogether'; +} from 'instantsearch-core'; export type UseFrequentlyBoughtTogetherProps = FrequentlyBoughtTogetherConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts b/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts index 57b69fb7dcd..1983c444da8 100644 --- a/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts +++ b/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts @@ -1,14 +1,14 @@ -import connectGeoSearch from 'instantsearch.js/es/connectors/geo-search/connectGeoSearch'; +import { connectGeoSearch as connectGeoSearch } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { GeoHit } from 'instantsearch.js'; +import type { GeoHit } from 'instantsearch-core'; import type { GeoSearchConnector, GeoSearchConnectorParams, GeoSearchWidgetDescription, -} from 'instantsearch.js/es/connectors/geo-search/connectGeoSearch'; +} from 'instantsearch-core'; export type UseGeoSearchProps = GeoSearchConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useHierarchicalMenu.ts b/packages/react-instantsearch-core/src/connectors/useHierarchicalMenu.ts index b10cd55d483..ed38f5b474d 100644 --- a/packages/react-instantsearch-core/src/connectors/useHierarchicalMenu.ts +++ b/packages/react-instantsearch-core/src/connectors/useHierarchicalMenu.ts @@ -1,4 +1,4 @@ -import connectHierarchicalMenu from 'instantsearch.js/es/connectors/hierarchical-menu/connectHierarchicalMenu'; +import { connectHierarchicalMenu as connectHierarchicalMenu } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { HierarchicalMenuConnectorParams, HierarchicalMenuWidgetDescription, -} from 'instantsearch.js/es/connectors/hierarchical-menu/connectHierarchicalMenu'; +} from 'instantsearch-core'; export type UseHierarchicalMenuProps = HierarchicalMenuConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useHits.ts b/packages/react-instantsearch-core/src/connectors/useHits.ts index a24b439f2c6..8224f4d38d4 100644 --- a/packages/react-instantsearch-core/src/connectors/useHits.ts +++ b/packages/react-instantsearch-core/src/connectors/useHits.ts @@ -1,14 +1,14 @@ -import connectHits from 'instantsearch.js/es/connectors/hits/connectHits'; +import { connectHits as connectHits } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { BaseHit } from 'instantsearch.js'; +import type { BaseHit } from 'instantsearch-core'; import type { HitsConnectorParams, HitsWidgetDescription, HitsConnector, -} from 'instantsearch.js/es/connectors/hits/connectHits'; +} from 'instantsearch-core'; export type UseHitsProps = HitsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useHitsPerPage.ts b/packages/react-instantsearch-core/src/connectors/useHitsPerPage.ts index dafa5260d2d..5860bfa7e62 100644 --- a/packages/react-instantsearch-core/src/connectors/useHitsPerPage.ts +++ b/packages/react-instantsearch-core/src/connectors/useHitsPerPage.ts @@ -1,4 +1,4 @@ -import connectHitsPerPage from 'instantsearch.js/es/connectors/hits-per-page/connectHitsPerPage'; +import { connectHitsPerPage as connectHitsPerPage } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { HitsPerPageConnectorParams, HitsPerPageWidgetDescription, -} from 'instantsearch.js/es/connectors/hits-per-page/connectHitsPerPage'; +} from 'instantsearch-core'; export type UseHitsPerPageProps = HitsPerPageConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useInfiniteHits.ts b/packages/react-instantsearch-core/src/connectors/useInfiniteHits.ts index f5bb0454abe..a2e7b811575 100644 --- a/packages/react-instantsearch-core/src/connectors/useInfiniteHits.ts +++ b/packages/react-instantsearch-core/src/connectors/useInfiniteHits.ts @@ -1,14 +1,14 @@ -import connectInfiniteHits from 'instantsearch.js/es/connectors/infinite-hits/connectInfiniteHits'; +import { connectInfiniteHits as connectInfiniteHits } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { BaseHit } from 'instantsearch.js'; +import type { BaseHit } from 'instantsearch-core'; import type { InfiniteHitsConnectorParams, InfiniteHitsWidgetDescription, InfiniteHitsConnector, -} from 'instantsearch.js/es/connectors/infinite-hits/connectInfiniteHits'; +} from 'instantsearch-core'; export type UseInfiniteHitsProps = InfiniteHitsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useLookingSimilar.ts b/packages/react-instantsearch-core/src/connectors/useLookingSimilar.ts index ddd4efd3d3b..de7a6decfb3 100644 --- a/packages/react-instantsearch-core/src/connectors/useLookingSimilar.ts +++ b/packages/react-instantsearch-core/src/connectors/useLookingSimilar.ts @@ -1,14 +1,14 @@ -import connectLookingSimilar from 'instantsearch.js/es/connectors/looking-similar/connectLookingSimilar'; +import { connectLookingSimilar as connectLookingSimilar } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { BaseHit } from 'instantsearch.js'; +import type { BaseHit } from 'instantsearch-core'; import type { LookingSimilarConnector, LookingSimilarConnectorParams, LookingSimilarWidgetDescription, -} from 'instantsearch.js/es/connectors/looking-similar/connectLookingSimilar'; +} from 'instantsearch-core'; export type UseLookingSimilarProps = LookingSimilarConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useMenu.ts b/packages/react-instantsearch-core/src/connectors/useMenu.ts index d8485060fba..7f1abadb4eb 100644 --- a/packages/react-instantsearch-core/src/connectors/useMenu.ts +++ b/packages/react-instantsearch-core/src/connectors/useMenu.ts @@ -1,4 +1,4 @@ -import connectMenu from 'instantsearch.js/es/connectors/menu/connectMenu'; +import { connectMenu as connectMenu } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { MenuConnectorParams, MenuWidgetDescription, -} from 'instantsearch.js/es/connectors/menu/connectMenu'; +} from 'instantsearch-core'; export type UseMenuProps = MenuConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useNumericMenu.ts b/packages/react-instantsearch-core/src/connectors/useNumericMenu.ts index b732020772f..343f00f1522 100644 --- a/packages/react-instantsearch-core/src/connectors/useNumericMenu.ts +++ b/packages/react-instantsearch-core/src/connectors/useNumericMenu.ts @@ -1,4 +1,4 @@ -import connectNumericMenu from 'instantsearch.js/es/connectors/numeric-menu/connectNumericMenu'; +import { connectNumericMenu as connectNumericMenu } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { NumericMenuConnectorParams, NumericMenuWidgetDescription, -} from 'instantsearch.js/es/connectors/numeric-menu/connectNumericMenu'; +} from 'instantsearch-core'; export type UseNumericMenuProps = NumericMenuConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/usePagination.ts b/packages/react-instantsearch-core/src/connectors/usePagination.ts index 3e9ca5b9953..4a4af1abcc3 100644 --- a/packages/react-instantsearch-core/src/connectors/usePagination.ts +++ b/packages/react-instantsearch-core/src/connectors/usePagination.ts @@ -1,4 +1,4 @@ -import connectPagination from 'instantsearch.js/es/connectors/pagination/connectPagination'; +import { connectPagination as connectPagination } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { PaginationConnectorParams, PaginationWidgetDescription, -} from 'instantsearch.js/es/connectors/pagination/connectPagination'; +} from 'instantsearch-core'; export type UsePaginationProps = PaginationConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/usePoweredBy.ts b/packages/react-instantsearch-core/src/connectors/usePoweredBy.ts index 1819eb3759e..5b11c76b771 100644 --- a/packages/react-instantsearch-core/src/connectors/usePoweredBy.ts +++ b/packages/react-instantsearch-core/src/connectors/usePoweredBy.ts @@ -1,6 +1,6 @@ -import { safelyRunOnBrowser } from 'instantsearch.js/es/lib/utils'; +import { safelyRunOnBrowser } from 'instantsearch-core'; -import type { PoweredByRenderState } from 'instantsearch.js/es/connectors/powered-by/connectPoweredBy'; +import type { PoweredByRenderState } from 'instantsearch-core'; export function usePoweredBy(): PoweredByRenderState { const hostname = safelyRunOnBrowser( diff --git a/packages/react-instantsearch-core/src/connectors/useQueryRules.ts b/packages/react-instantsearch-core/src/connectors/useQueryRules.ts index fe5195412d1..51e37f7ff46 100644 --- a/packages/react-instantsearch-core/src/connectors/useQueryRules.ts +++ b/packages/react-instantsearch-core/src/connectors/useQueryRules.ts @@ -1,4 +1,4 @@ -import connectQueryRules from 'instantsearch.js/es/connectors/query-rules/connectQueryRules'; +import { connectQueryRules as connectQueryRules } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { QueryRulesConnectorParams, QueryRulesWidgetDescription, -} from 'instantsearch.js/es/connectors/query-rules/connectQueryRules'; +} from 'instantsearch-core'; export type UseQueryRulesProps = QueryRulesConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useRange.ts b/packages/react-instantsearch-core/src/connectors/useRange.ts index a35501616a5..7ca697c9188 100644 --- a/packages/react-instantsearch-core/src/connectors/useRange.ts +++ b/packages/react-instantsearch-core/src/connectors/useRange.ts @@ -1,4 +1,4 @@ -import connectRange from 'instantsearch.js/es/connectors/range/connectRange'; +import { connectRange as connectRange } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { RangeConnectorParams, RangeWidgetDescription, -} from 'instantsearch.js/es/connectors/range/connectRange'; +} from 'instantsearch-core'; export type UseRangeProps = RangeConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useRefinementList.ts b/packages/react-instantsearch-core/src/connectors/useRefinementList.ts index 71e44054488..ab30c5a0583 100644 --- a/packages/react-instantsearch-core/src/connectors/useRefinementList.ts +++ b/packages/react-instantsearch-core/src/connectors/useRefinementList.ts @@ -1,4 +1,4 @@ -import connectRefinementList from 'instantsearch.js/es/connectors/refinement-list/connectRefinementList'; +import { connectRefinementList as connectRefinementList } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { RefinementListConnectorParams, RefinementListWidgetDescription, -} from 'instantsearch.js/es/connectors/refinement-list/connectRefinementList'; +} from 'instantsearch-core'; export type UseRefinementListProps = RefinementListConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useRelatedProducts.ts b/packages/react-instantsearch-core/src/connectors/useRelatedProducts.ts index 653a554e756..2304a65e0ba 100644 --- a/packages/react-instantsearch-core/src/connectors/useRelatedProducts.ts +++ b/packages/react-instantsearch-core/src/connectors/useRelatedProducts.ts @@ -1,14 +1,14 @@ -import connectRelatedProducts from 'instantsearch.js/es/connectors/related-products/connectRelatedProducts'; +import { connectRelatedProducts as connectRelatedProducts } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { BaseHit } from 'instantsearch.js'; +import type { BaseHit } from 'instantsearch-core'; import type { RelatedProductsConnector, RelatedProductsConnectorParams, RelatedProductsWidgetDescription, -} from 'instantsearch.js/es/connectors/related-products/connectRelatedProducts'; +} from 'instantsearch-core'; export type UseRelatedProductsProps = RelatedProductsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useSearchBox.ts b/packages/react-instantsearch-core/src/connectors/useSearchBox.ts index a9b242c9ebb..cc11570223f 100644 --- a/packages/react-instantsearch-core/src/connectors/useSearchBox.ts +++ b/packages/react-instantsearch-core/src/connectors/useSearchBox.ts @@ -1,4 +1,4 @@ -import connectSearchBox from 'instantsearch.js/es/connectors/search-box/connectSearchBox'; +import { connectSearchBox as connectSearchBox } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { SearchBoxConnectorParams, SearchBoxWidgetDescription, -} from 'instantsearch.js/es/connectors/search-box/connectSearchBox'; +} from 'instantsearch-core'; export type UseSearchBoxProps = SearchBoxConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useSortBy.ts b/packages/react-instantsearch-core/src/connectors/useSortBy.ts index ee9c2a09703..8dfc9e18965 100644 --- a/packages/react-instantsearch-core/src/connectors/useSortBy.ts +++ b/packages/react-instantsearch-core/src/connectors/useSortBy.ts @@ -1,4 +1,4 @@ -import connectSortBy from 'instantsearch.js/es/connectors/sort-by/connectSortBy'; +import { connectSortBy as connectSortBy } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { SortByConnectorParams, SortByWidgetDescription, -} from 'instantsearch.js/es/connectors/sort-by/connectSortBy'; +} from 'instantsearch-core'; export type UseSortByProps = SortByConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useStats.ts b/packages/react-instantsearch-core/src/connectors/useStats.ts index ac5bd659c95..c76a0b4c0d9 100644 --- a/packages/react-instantsearch-core/src/connectors/useStats.ts +++ b/packages/react-instantsearch-core/src/connectors/useStats.ts @@ -1,4 +1,4 @@ -import connectStats from 'instantsearch.js/es/connectors/stats/connectStats'; +import { connectStats as connectStats } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { StatsConnectorParams, StatsWidgetDescription, -} from 'instantsearch.js/es/connectors/stats/connectStats'; +} from 'instantsearch-core'; export type UseStatsProps = StatsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useToggleRefinement.ts b/packages/react-instantsearch-core/src/connectors/useToggleRefinement.ts index 33830749ad6..c21a7d9034a 100644 --- a/packages/react-instantsearch-core/src/connectors/useToggleRefinement.ts +++ b/packages/react-instantsearch-core/src/connectors/useToggleRefinement.ts @@ -1,4 +1,4 @@ -import connectToggleRefinement from 'instantsearch.js/es/connectors/toggle-refinement/connectToggleRefinement'; +import { connectToggleRefinement as connectToggleRefinement } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -6,7 +6,7 @@ import type { AdditionalWidgetProperties } from '../hooks/useConnector'; import type { ToggleRefinementConnectorParams, ToggleRefinementWidgetDescription, -} from 'instantsearch.js/es/connectors/toggle-refinement/connectToggleRefinement'; +} from 'instantsearch-core'; export type UseToggleRefinementProps = ToggleRefinementConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useTrendingFacets.ts b/packages/react-instantsearch-core/src/connectors/useTrendingFacets.ts index 8c3d6d50da5..a87a24d7772 100644 --- a/packages/react-instantsearch-core/src/connectors/useTrendingFacets.ts +++ b/packages/react-instantsearch-core/src/connectors/useTrendingFacets.ts @@ -1,4 +1,4 @@ -import connectTrendingFacets from 'instantsearch.js/es/connectors/trending-facets/connectTrendingFacets'; +import { connectTrendingFacets as connectTrendingFacets } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; @@ -7,7 +7,7 @@ import type { TrendingFacetsConnector, TrendingFacetsConnectorParams, TrendingFacetsWidgetDescription, -} from 'instantsearch.js/es/connectors/trending-facets/connectTrendingFacets'; +} from 'instantsearch-core'; export type UseTrendingFacetsProps = TrendingFacetsConnectorParams; diff --git a/packages/react-instantsearch-core/src/connectors/useTrendingItems.ts b/packages/react-instantsearch-core/src/connectors/useTrendingItems.ts index 37543adf501..8528a665ad6 100644 --- a/packages/react-instantsearch-core/src/connectors/useTrendingItems.ts +++ b/packages/react-instantsearch-core/src/connectors/useTrendingItems.ts @@ -1,14 +1,14 @@ -import connectTrendingItems from 'instantsearch.js/es/connectors/trending-items/connectTrendingItems'; +import { connectTrendingItems as connectTrendingItems } from 'instantsearch-core'; import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { BaseHit } from 'instantsearch.js'; +import type { BaseHit } from 'instantsearch-core'; import type { TrendingItemsConnector, TrendingItemsConnectorParams, TrendingItemsWidgetDescription, -} from 'instantsearch.js/es/connectors/trending-items/connectTrendingItems'; +} from 'instantsearch-core'; export type UseTrendingItemsProps = TrendingItemsConnectorParams; diff --git a/packages/react-instantsearch-core/src/hooks/__tests__/useConnector.test.tsx b/packages/react-instantsearch-core/src/hooks/__tests__/useConnector.test.tsx index 377a022cc08..623edb5abe6 100644 --- a/packages/react-instantsearch-core/src/hooks/__tests__/useConnector.test.tsx +++ b/packages/react-instantsearch-core/src/hooks/__tests__/useConnector.test.tsx @@ -12,7 +12,7 @@ import { } from '@instantsearch/testutils'; import { render, waitFor, renderHook } from '@testing-library/react'; import { SearchParameters, SearchResults } from 'algoliasearch-helper'; -import connectHits from 'instantsearch.js/es/connectors/hits/connectHits'; +import { connectHits as connectHits } from 'instantsearch-core'; import React, { StrictMode, useState } from 'react'; import { Index } from '../../components/Index'; @@ -24,11 +24,11 @@ import { noop } from '../../lib/noop'; import { useConnector } from '../useConnector'; import type { UseHitsProps } from '../../connectors/useHits'; -import type { Connector } from 'instantsearch.js'; +import type { Connector } from 'instantsearch-core'; import type { HitsConnectorParams, HitsWidgetDescription, -} from 'instantsearch.js/es/connectors/hits/connectHits'; +} from 'instantsearch-core'; type CustomSearchBoxWidgetDescription = { $$type: 'test.searchBox'; diff --git a/packages/react-instantsearch-core/src/hooks/useConnector.ts b/packages/react-instantsearch-core/src/hooks/useConnector.ts index 9b8783d0ed9..f9179ac73cd 100644 --- a/packages/react-instantsearch-core/src/hooks/useConnector.ts +++ b/packages/react-instantsearch-core/src/hooks/useConnector.ts @@ -15,7 +15,7 @@ import type { UiState, Widget, WidgetDescription, -} from 'instantsearch.js'; +} from 'instantsearch-core'; export type AdditionalWidgetProperties = Partial> & { skipSuspense?: boolean; diff --git a/packages/react-instantsearch-core/src/hooks/useInstantSearch.ts b/packages/react-instantsearch-core/src/hooks/useInstantSearch.ts index 55cb997af13..042886ce0ad 100644 --- a/packages/react-instantsearch-core/src/hooks/useInstantSearch.ts +++ b/packages/react-instantsearch-core/src/hooks/useInstantSearch.ts @@ -7,7 +7,7 @@ import { useSearchState } from '../lib/useSearchState'; import type { SearchResultsApi } from '../lib/useSearchResults'; import type { SearchStateApi } from '../lib/useSearchState'; -import type { InstantSearch, Middleware, UiState } from 'instantsearch.js'; +import type { InstantSearch, Middleware, UiState } from 'instantsearch-core'; export type InstantSearchApi = SearchStateApi & diff --git a/packages/react-instantsearch-core/src/lib/IndexContext.ts b/packages/react-instantsearch-core/src/lib/IndexContext.ts index 5e1b3ce7ca3..a0831c4fac7 100644 --- a/packages/react-instantsearch-core/src/lib/IndexContext.ts +++ b/packages/react-instantsearch-core/src/lib/IndexContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { IndexWidget } from 'instantsearch-core'; export const IndexContext = createContext(null); diff --git a/packages/react-instantsearch-core/src/lib/InstantSearchContext.ts b/packages/react-instantsearch-core/src/lib/InstantSearchContext.ts index b5342b5472f..c4222c03543 100644 --- a/packages/react-instantsearch-core/src/lib/InstantSearchContext.ts +++ b/packages/react-instantsearch-core/src/lib/InstantSearchContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import type { InstantSearch } from 'instantsearch.js'; +import type { InstantSearch } from 'instantsearch-core'; export const InstantSearchContext = createContext(null); diff --git a/packages/react-instantsearch-core/src/lib/InstantSearchSSRContext.ts b/packages/react-instantsearch-core/src/lib/InstantSearchSSRContext.ts index a76c8b87673..12914d76510 100644 --- a/packages/react-instantsearch-core/src/lib/InstantSearchSSRContext.ts +++ b/packages/react-instantsearch-core/src/lib/InstantSearchSSRContext.ts @@ -2,7 +2,7 @@ import { createContext } from 'react'; import type { InstantSearchServerState } from '../components/InstantSearchSSRProvider'; import type { InternalInstantSearch } from './useInstantSearchApi'; -import type { UiState } from 'instantsearch.js'; +import type { UiState } from 'instantsearch-core'; import type { MutableRefObject } from 'react'; export type InstantSearchSSRContextApi< diff --git a/packages/react-instantsearch-core/src/lib/getIndexSearchResults.ts b/packages/react-instantsearch-core/src/lib/getIndexSearchResults.ts index f8845751a58..fe7cf82cef4 100644 --- a/packages/react-instantsearch-core/src/lib/getIndexSearchResults.ts +++ b/packages/react-instantsearch-core/src/lib/getIndexSearchResults.ts @@ -1,6 +1,6 @@ import { createSearchResults } from './createSearchResults'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { IndexWidget } from 'instantsearch-core'; export function getIndexSearchResults(indexWidget: IndexWidget) { const helper = indexWidget.getHelper()!; diff --git a/packages/react-instantsearch-core/src/lib/useAppIdAndApiKey.ts b/packages/react-instantsearch-core/src/lib/useAppIdAndApiKey.ts index e658632f59e..e8f31c210cb 100644 --- a/packages/react-instantsearch-core/src/lib/useAppIdAndApiKey.ts +++ b/packages/react-instantsearch-core/src/lib/useAppIdAndApiKey.ts @@ -1,4 +1,4 @@ -import { getAppIdAndApiKey } from 'instantsearch.js/es/lib/utils'; +import { getAppIdAndApiKey } from 'instantsearch-core'; import { useInstantSearchContext } from './useInstantSearchContext'; diff --git a/packages/react-instantsearch-core/src/lib/useIndex.ts b/packages/react-instantsearch-core/src/lib/useIndex.ts index edeb54e4da0..eb1772c5fd9 100644 --- a/packages/react-instantsearch-core/src/lib/useIndex.ts +++ b/packages/react-instantsearch-core/src/lib/useIndex.ts @@ -1,4 +1,4 @@ -import index from 'instantsearch.js/es/widgets/index/index'; +import { index as index } from 'instantsearch-core'; import { useMemo } from 'react'; import { useForceUpdate } from './useForceUpdate'; @@ -9,7 +9,7 @@ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; import { useStableValue } from './useStableValue'; import { useWidget } from './useWidget'; -import type { IndexWidgetParams } from 'instantsearch.js/es/widgets/index/index'; +import type { IndexWidgetParams } from 'instantsearch-core'; export type UseIndexProps = IndexWidgetParams; diff --git a/packages/react-instantsearch-core/src/lib/useIndexContext.ts b/packages/react-instantsearch-core/src/lib/useIndexContext.ts index 2a8edce4ac2..869c26193a1 100644 --- a/packages/react-instantsearch-core/src/lib/useIndexContext.ts +++ b/packages/react-instantsearch-core/src/lib/useIndexContext.ts @@ -3,7 +3,7 @@ import { useContext } from 'react'; import { IndexContext } from './IndexContext'; import { invariant } from './invariant'; -import type { IndexWidget, UiState } from 'instantsearch.js'; +import type { IndexWidget, UiState } from 'instantsearch-core'; import type { Context } from 'react'; export function useIndexContext() { diff --git a/packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts b/packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts index 8269c64c47e..7bd4469330b 100644 --- a/packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts +++ b/packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts @@ -1,6 +1,4 @@ -import InstantSearch, { - INSTANTSEARCH_FUTURE_DEFAULTS, -} from 'instantsearch.js/es/lib/InstantSearch'; +import { InstantSearch, INSTANTSEARCH_FUTURE_DEFAULTS } from 'instantsearch-core'; import { useCallback, useRef, version as ReactVersion } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; @@ -18,7 +16,7 @@ import type { InstantSearchOptions, SearchClient, UiState, -} from 'instantsearch.js'; +} from 'instantsearch-core'; const defaultUserAgents = [ `react (${ReactVersion})`, @@ -82,7 +80,7 @@ export function useInstantSearchApi( // We don't use the `instantsearch()` function because it comes with other // top-level APIs that we don't need. // See https://github.com/algolia/instantsearch/blob/5b529f43d8acc680f85837eaaa41f7fd03a3f833/src/index.es.ts#L63-L86 - const search = new InstantSearch(props) as InternalInstantSearch< + const search = new InstantSearch(props) as unknown as InternalInstantSearch< TUiState, TRouteState >; diff --git a/packages/react-instantsearch-core/src/lib/useInstantSearchContext.ts b/packages/react-instantsearch-core/src/lib/useInstantSearchContext.ts index ef2a7126f1c..becfc9a5cd6 100644 --- a/packages/react-instantsearch-core/src/lib/useInstantSearchContext.ts +++ b/packages/react-instantsearch-core/src/lib/useInstantSearchContext.ts @@ -4,7 +4,7 @@ import { InstantSearchContext } from './InstantSearchContext'; import { invariant } from './invariant'; import type { InternalInstantSearch } from './useInstantSearchApi'; -import type { UiState } from 'instantsearch.js'; +import type { UiState } from 'instantsearch-core'; import type { Context } from 'react'; export function useInstantSearchContext< diff --git a/packages/react-instantsearch-core/src/lib/useInstantSearchSSRContext.ts b/packages/react-instantsearch-core/src/lib/useInstantSearchSSRContext.ts index 70780d79e54..3cc773f8bb8 100644 --- a/packages/react-instantsearch-core/src/lib/useInstantSearchSSRContext.ts +++ b/packages/react-instantsearch-core/src/lib/useInstantSearchSSRContext.ts @@ -3,7 +3,7 @@ import { useContext } from 'react'; import { InstantSearchSSRContext } from './InstantSearchSSRContext'; import type { InstantSearchSSRContextApi } from './InstantSearchSSRContext'; -import type { UiState } from 'instantsearch.js'; +import type { UiState } from 'instantsearch-core'; import type { Context } from 'react'; export function useInstantSearchSSRContext< diff --git a/packages/react-instantsearch-core/src/lib/useInstantSearchServerContext.ts b/packages/react-instantsearch-core/src/lib/useInstantSearchServerContext.ts index 3665707e360..4811a7b330f 100644 --- a/packages/react-instantsearch-core/src/lib/useInstantSearchServerContext.ts +++ b/packages/react-instantsearch-core/src/lib/useInstantSearchServerContext.ts @@ -3,7 +3,7 @@ import { useContext } from 'react'; import { InstantSearchServerContext } from '../components/InstantSearchServerContext'; import type { InstantSearchServerContextApi } from '../components/InstantSearchServerContext'; -import type { UiState } from 'instantsearch.js'; +import type { UiState } from 'instantsearch-core'; import type { Context } from 'react'; export function useInstantSearchServerContext< diff --git a/packages/react-instantsearch-core/src/lib/useSearchResults.ts b/packages/react-instantsearch-core/src/lib/useSearchResults.ts index bb3d73a43da..34692512264 100644 --- a/packages/react-instantsearch-core/src/lib/useSearchResults.ts +++ b/packages/react-instantsearch-core/src/lib/useSearchResults.ts @@ -1,4 +1,4 @@ -import { isIndexWidget } from 'instantsearch.js/es/lib/utils'; +import { isIndexWidget } from 'instantsearch-core'; import { useEffect, useState } from 'react'; import { getIndexSearchResults } from './getIndexSearchResults'; @@ -6,7 +6,7 @@ import { useIndexContext } from './useIndexContext'; import { useInstantSearchContext } from './useInstantSearchContext'; import type { SearchResults } from 'algoliasearch-helper'; -import type { ScopedResult } from 'instantsearch.js'; +import type { ScopedResult } from 'instantsearch-core'; export type SearchResultsApi = { results: SearchResults; diff --git a/packages/react-instantsearch-core/src/lib/useSearchState.ts b/packages/react-instantsearch-core/src/lib/useSearchState.ts index 1bc3c61d1df..817c7530e82 100644 --- a/packages/react-instantsearch-core/src/lib/useSearchState.ts +++ b/packages/react-instantsearch-core/src/lib/useSearchState.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useIndexContext } from './useIndexContext'; import { useInstantSearchContext } from './useInstantSearchContext'; -import type { InstantSearch, UiState, IndexWidget } from 'instantsearch.js'; +import type { InstantSearch, UiState, IndexWidget } from 'instantsearch-core'; export type SearchStateApi = { uiState: TUiState; diff --git a/packages/react-instantsearch-core/src/lib/useWidget.ts b/packages/react-instantsearch-core/src/lib/useWidget.ts index b79e6325028..d95ea729bed 100644 --- a/packages/react-instantsearch-core/src/lib/useWidget.ts +++ b/packages/react-instantsearch-core/src/lib/useWidget.ts @@ -1,4 +1,4 @@ -import { isTwoPassWidget } from 'instantsearch.js/es/lib/utils'; +import { isTwoPassWidget } from 'instantsearch-core'; import { useEffect, useRef } from 'react'; import { dequal } from './dequal'; @@ -8,8 +8,8 @@ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; import { useRSCContext } from './useRSCContext'; import { warn } from './warn'; -import type { Widget } from 'instantsearch.js'; -import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index'; +import type { Widget } from 'instantsearch-core'; +import type { IndexWidget } from 'instantsearch-core'; export function useWidget({ widget, diff --git a/packages/react-instantsearch-core/src/server/__tests__/getServerState.test.tsx b/packages/react-instantsearch-core/src/server/__tests__/getServerState.test.tsx index ac0e76d6d0e..b165d0d8ad1 100644 --- a/packages/react-instantsearch-core/src/server/__tests__/getServerState.test.tsx +++ b/packages/react-instantsearch-core/src/server/__tests__/getServerState.test.tsx @@ -22,7 +22,7 @@ import { import { getServerState } from '../getServerState'; import type { MockSearchClient } from '@instantsearch/mocks'; -import type { Hit as AlgoliaHit } from 'instantsearch.js'; +import type { Hit as AlgoliaHit } from 'instantsearch-core'; import type { InstantSearchServerState, InstantSearchProps, diff --git a/packages/react-instantsearch-core/src/server/getServerState.tsx b/packages/react-instantsearch-core/src/server/getServerState.tsx index 6d43cedac5e..0a199c393b2 100644 --- a/packages/react-instantsearch-core/src/server/getServerState.tsx +++ b/packages/react-instantsearch-core/src/server/getServerState.tsx @@ -1,12 +1,12 @@ import { getInitialResults, waitForResults, -} from 'instantsearch.js/es/lib/server'; +} from 'instantsearch-core'; import { isTwoPassWidget, walkIndex, resetWidgetId, -} from 'instantsearch.js/es/lib/utils'; +} from 'instantsearch-core'; import React from 'react'; import { InstantSearchServerContext } from '../components/InstantSearchServerContext'; @@ -14,8 +14,8 @@ import { InstantSearchSSRProvider } from '../components/InstantSearchSSRProvider import type { InstantSearchServerContextApi } from '../components/InstantSearchServerContext'; import type { InstantSearchServerState } from '../components/InstantSearchSSRProvider'; -import type { InstantSearch, UiState } from 'instantsearch.js'; -import type { ReactNode } from 'react'; +import type { InstantSearch, UiState } from 'instantsearch-core'; +import type { JSX, ReactNode } from 'react'; type SearchRef = { current: InstantSearch | undefined }; diff --git a/packages/react-instantsearch-nextjs/package.json b/packages/react-instantsearch-nextjs/package.json index ecb9459bfa5..92fe4c8ed43 100644 --- a/packages/react-instantsearch-nextjs/package.json +++ b/packages/react-instantsearch-nextjs/package.json @@ -48,7 +48,8 @@ "watch:es": "rollup -c rollup.config.mjs --watch" }, "dependencies": { - "@swc/helpers": "0.5.18" + "@swc/helpers": "0.5.18", + "instantsearch-core": "0.1.0" }, "devDependencies": { "@playwright/test": "1.49.1", diff --git a/packages/react-instantsearch-nextjs/src/InitializePromise.ts b/packages/react-instantsearch-nextjs/src/InitializePromise.ts index 0cc63a0dc3f..c1f24099d16 100644 --- a/packages/react-instantsearch-nextjs/src/InitializePromise.ts +++ b/packages/react-instantsearch-nextjs/src/InitializePromise.ts @@ -1,9 +1,9 @@ -import { getInitialResults } from 'instantsearch.js/es/lib/server'; +import { getInitialResults } from 'instantsearch-core'; import { isTwoPassWidget, walkIndex, resetWidgetId, -} from 'instantsearch.js/es/lib/utils'; +} from 'instantsearch-core'; import { ServerInsertedHTMLContext } from 'next/navigation'; import { useContext } from 'react'; import { @@ -18,7 +18,7 @@ import type { SearchOptions, CompositionClient, SearchClient, -} from 'instantsearch.js'; +} from 'instantsearch-core'; type InitializePromiseProps = { /** diff --git a/packages/react-instantsearch-nextjs/src/InstantSearchNext.tsx b/packages/react-instantsearch-nextjs/src/InstantSearchNext.tsx index e18da65672c..838605e8ae6 100644 --- a/packages/react-instantsearch-nextjs/src/InstantSearchNext.tsx +++ b/packages/react-instantsearch-nextjs/src/InstantSearchNext.tsx @@ -1,4 +1,4 @@ -import { safelyRunOnBrowser } from 'instantsearch.js/es/lib/utils'; +import { safelyRunOnBrowser } from 'instantsearch-core'; import React, { useEffect, useRef, useState } from 'react'; import { InstantSearch, @@ -12,8 +12,8 @@ import { useDynamicRouteWarning } from './useDynamicRouteWarning'; import { useInstantSearchRouting } from './useInstantSearchRouting'; import { useNextHeaders } from './useNextHeaders'; -import type { InitialResults, StateMapping, UiState } from 'instantsearch.js'; -import type { BrowserHistoryArgs } from 'instantsearch.js/es/lib/routers/history'; +import type { InitialResults, StateMapping, UiState } from 'instantsearch-core'; +import type { BrowserHistoryArgs } from 'instantsearch-core'; import type { InstantSearchProps, InstantSearchSSRContextApi, diff --git a/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise-composition.test.tsx b/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise-composition.test.tsx index b70ae0e3934..35c975da909 100644 --- a/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise-composition.test.tsx +++ b/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise-composition.test.tsx @@ -18,8 +18,8 @@ import { TriggerSearch } from '../TriggerSearch'; import type { PromiseWithState } from 'react-instantsearch-core'; -jest.mock('instantsearch.js/es/lib/utils', () => ({ - ...jest.requireActual('instantsearch.js/es/lib/utils'), +jest.mock('instantsearch-core', () => ({ + ...jest.requireActual('instantsearch-core'), resetWidgetId: jest.fn(), })); diff --git a/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise.test.tsx b/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise.test.tsx index 79b6ae6f151..d011fae57b3 100644 --- a/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise.test.tsx +++ b/packages/react-instantsearch-nextjs/src/__tests__/InitializePromise.test.tsx @@ -7,7 +7,7 @@ import { createSingleSearchResponse, } from '@instantsearch/mocks'; import { render, act } from '@testing-library/react'; -import * as utils from 'instantsearch.js/es/lib/utils'; +import * as utils from 'instantsearch-core'; import { ServerInsertedHTMLContext } from 'next/navigation'; import React from 'react'; import { SearchBox, TrendingItems } from 'react-instantsearch'; @@ -22,8 +22,8 @@ import { TriggerSearch } from '../TriggerSearch'; import type { PromiseWithState } from 'react-instantsearch-core'; -jest.mock('instantsearch.js/es/lib/utils', () => ({ - ...jest.requireActual('instantsearch.js/es/lib/utils'), +jest.mock('instantsearch-core', () => ({ + ...jest.requireActual('instantsearch-core'), resetWidgetId: jest.fn(), })); diff --git a/packages/react-instantsearch-nextjs/src/__tests__/InstantSearchNext.test.tsx b/packages/react-instantsearch-nextjs/src/__tests__/InstantSearchNext.test.tsx index ae2a2845538..2d57e5c2551 100644 --- a/packages/react-instantsearch-nextjs/src/__tests__/InstantSearchNext.test.tsx +++ b/packages/react-instantsearch-nextjs/src/__tests__/InstantSearchNext.test.tsx @@ -10,7 +10,7 @@ import { SearchBox } from 'react-instantsearch'; import { InstantSearchNext } from '../InstantSearchNext'; -import type { InitialResults } from 'instantsearch.js'; +import type { InitialResults } from 'instantsearch-core'; const InstantSearchInitialResults = Symbol.for('InstantSearchInitialResults'); declare global { diff --git a/packages/react-instantsearch-nextjs/src/createInsertHTML.tsx b/packages/react-instantsearch-nextjs/src/createInsertHTML.tsx index 4678eb1d67c..9b414cd0127 100644 --- a/packages/react-instantsearch-nextjs/src/createInsertHTML.tsx +++ b/packages/react-instantsearch-nextjs/src/createInsertHTML.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { htmlEscapeJsonString } from './htmlEscape'; -import type { InitialResults } from 'instantsearch.js'; +import type { InitialResults } from 'instantsearch-core'; export const createInsertHTML = ({ diff --git a/packages/react-instantsearch-nextjs/src/useInstantSearchRouting.ts b/packages/react-instantsearch-nextjs/src/useInstantSearchRouting.ts index 9bde4fb2274..1a0e9b55d51 100644 --- a/packages/react-instantsearch-nextjs/src/useInstantSearchRouting.ts +++ b/packages/react-instantsearch-nextjs/src/useInstantSearchRouting.ts @@ -1,12 +1,12 @@ -import historyRouter from 'instantsearch.js/es/lib/routers/history'; +import { history as historyRouter } from 'instantsearch-core'; import { usePathname, useSearchParams } from 'next/navigation'; import { useRef, useEffect } from 'react'; import { useNextHeaders } from './useNextHeaders'; import type { InstantSearchNextProps } from './InstantSearchNext'; -import type { UiState } from 'instantsearch.js'; -import type { BrowserHistoryArgs } from 'instantsearch.js/es/lib/routers/history'; +import type { UiState } from 'instantsearch-core'; +import type { BrowserHistoryArgs } from 'instantsearch-core'; import type { InstantSearchProps } from 'react-instantsearch-core'; export function useInstantSearchRouting< diff --git a/packages/react-instantsearch-router-nextjs/package.json b/packages/react-instantsearch-router-nextjs/package.json index 3f754799ba3..41bb13bcf56 100644 --- a/packages/react-instantsearch-router-nextjs/package.json +++ b/packages/react-instantsearch-router-nextjs/package.json @@ -47,8 +47,8 @@ }, "dependencies": { "@swc/helpers": "0.5.18", - "instantsearch.js": "4.103.0", - "react-instantsearch-core": "7.37.0" + "react-instantsearch-core": "7.37.0", + "instantsearch-core": "0.1.0" }, "devDependencies": { "@playwright/test": "1.49.1" diff --git a/packages/react-instantsearch-router-nextjs/src/index.ts b/packages/react-instantsearch-router-nextjs/src/index.ts index 6583138c347..811889f4095 100644 --- a/packages/react-instantsearch-router-nextjs/src/index.ts +++ b/packages/react-instantsearch-router-nextjs/src/index.ts @@ -1,9 +1,9 @@ -import history from 'instantsearch.js/es/lib/routers/history'; +import { history as history } from 'instantsearch-core'; import { stripLocaleFromUrl } from './utils/stripLocaleFromUrl'; -import type { Router, UiState } from 'instantsearch.js'; -import type { BrowserHistoryArgs } from 'instantsearch.js/es/lib/routers/history'; +import type { Router, UiState } from 'instantsearch-core'; +import type { BrowserHistoryArgs } from 'instantsearch-core'; import type { Router as NextRouter, SingletonRouter } from 'next/router'; type BeforePopStateCallback = NonNullable; diff --git a/packages/react-instantsearch/package.json b/packages/react-instantsearch/package.json index 9d1e3390b60..90f6d1244b6 100644 --- a/packages/react-instantsearch/package.json +++ b/packages/react-instantsearch/package.json @@ -46,8 +46,8 @@ "dependencies": { "@swc/helpers": "0.5.18", "instantsearch-ui-components": "0.32.0", - "instantsearch.js": "4.103.0", - "react-instantsearch-core": "7.37.0" + "react-instantsearch-core": "7.37.0", + "instantsearch-core": "0.1.0" }, "peerDependencies": { "algoliasearch": ">= 3.1 < 6", diff --git a/packages/react-instantsearch/src/__tests__/common-connectors.test.tsx b/packages/react-instantsearch/src/__tests__/common-connectors.test.tsx index 8765e9a7e3d..4831d889967 100644 --- a/packages/react-instantsearch/src/__tests__/common-connectors.test.tsx +++ b/packages/react-instantsearch/src/__tests__/common-connectors.test.tsx @@ -4,7 +4,7 @@ import { runTestSuites } from '@instantsearch/tests'; import * as suites from '@instantsearch/tests/connectors'; import { act, render } from '@testing-library/react'; -import { connectRatingMenu } from 'instantsearch.js/es/connectors'; +import { connectRatingMenu } from 'instantsearch-core'; import React, { useState } from 'react'; import { @@ -49,7 +49,7 @@ import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests'; import type { RatingMenuConnectorParams, RatingMenuWidgetDescription, -} from 'instantsearch.js/es/connectors/rating-menu/connectRatingMenu'; +} from 'instantsearch-core'; type TestSuites = typeof suites; const testSuites: TestSuites = suites; diff --git a/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx b/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx index 55a74c5a360..852b4b9161d 100644 --- a/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx +++ b/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx @@ -39,8 +39,8 @@ import { } from '..'; import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests'; -import type { Hit } from 'instantsearch.js'; -import type { SendEventForHits } from 'instantsearch.js/es/lib/utils'; +import type { Hit } from 'instantsearch-core'; +import type { SendEventForHits } from 'instantsearch-core'; type TestSuites = typeof suites; const testSuites: TestSuites = suites; @@ -231,8 +231,8 @@ const testSetups: TestSetupsMap = { return { algoliaAgents: [ - `instantsearch.js (${ - require('../../../instantsearch.js/package.json').version + `instantsearch-core (${ + require('../../../instantsearch-core/package.json').version })`, `react-instantsearch (${ require('../../../react-instantsearch-core/package.json').version diff --git a/packages/react-instantsearch/src/ui/CurrentRefinements.tsx b/packages/react-instantsearch/src/ui/CurrentRefinements.tsx index 1493d12b63d..8aa49e16139 100644 --- a/packages/react-instantsearch/src/ui/CurrentRefinements.tsx +++ b/packages/react-instantsearch/src/ui/CurrentRefinements.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { capitalize, isModifierClick } from './lib'; -import type { CurrentRefinementsConnectorParamsItem } from 'instantsearch.js/es/connectors/current-refinements/connectCurrentRefinements'; +import type { CurrentRefinementsConnectorParamsItem } from 'instantsearch-core'; export type CurrentRefinementsProps = React.ComponentProps<'div'> & { classNames?: Partial; diff --git a/packages/react-instantsearch/src/ui/HitsPerPage.tsx b/packages/react-instantsearch/src/ui/HitsPerPage.tsx index 2aa8750617c..27b67a05a8b 100644 --- a/packages/react-instantsearch/src/ui/HitsPerPage.tsx +++ b/packages/react-instantsearch/src/ui/HitsPerPage.tsx @@ -1,7 +1,7 @@ import { cx } from 'instantsearch-ui-components'; import React from 'react'; -import type { HitsPerPageConnectorParamsItem as HitsPerPageItem } from 'instantsearch.js/es/connectors/hits-per-page/connectHitsPerPage'; +import type { HitsPerPageConnectorParamsItem as HitsPerPageItem } from 'instantsearch-core'; export type HitsPerPageProps = Omit, 'onChange'> & { items: HitsPerPageItem[]; diff --git a/packages/react-instantsearch/src/ui/InfiniteHits.tsx b/packages/react-instantsearch/src/ui/InfiniteHits.tsx index b2610f8bf0f..a5240f5d112 100644 --- a/packages/react-instantsearch/src/ui/InfiniteHits.tsx +++ b/packages/react-instantsearch/src/ui/InfiniteHits.tsx @@ -2,8 +2,8 @@ import { cx } from 'instantsearch-ui-components'; import React from 'react'; import type { Banner } from 'algoliasearch-helper'; -import type { Hit } from 'instantsearch.js'; -import type { SendEventForHits } from 'instantsearch.js/es/lib/utils'; +import type { Hit } from 'instantsearch-core'; +import type { SendEventForHits } from 'instantsearch-core'; export type InfiniteHitsProps = React.ComponentProps<'div'> & { hits: THit[]; diff --git a/packages/react-instantsearch/src/ui/Menu.tsx b/packages/react-instantsearch/src/ui/Menu.tsx index 934d4f5276f..48e6910c2b7 100644 --- a/packages/react-instantsearch/src/ui/Menu.tsx +++ b/packages/react-instantsearch/src/ui/Menu.tsx @@ -5,8 +5,8 @@ import { isModifierClick } from './lib/isModifierClick'; import { ShowMoreButton } from './ShowMoreButton'; import type { ShowMoreButtonTranslations } from './ShowMoreButton'; -import type { CreateURL } from 'instantsearch.js'; -import type { MenuItem } from 'instantsearch.js/es/connectors/menu/connectMenu'; +import type { CreateURL } from 'instantsearch-core'; +import type { MenuItem } from 'instantsearch-core'; export type MenuProps = React.ComponentProps<'div'> & { items: MenuItem[]; diff --git a/packages/react-instantsearch/src/ui/Pagination.tsx b/packages/react-instantsearch/src/ui/Pagination.tsx index 17fd94395de..eac0280f13d 100644 --- a/packages/react-instantsearch/src/ui/Pagination.tsx +++ b/packages/react-instantsearch/src/ui/Pagination.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { isModifierClick } from './lib/isModifierClick'; -import type { CreateURL } from 'instantsearch.js'; +import type { CreateURL } from 'instantsearch-core'; export type PageItemTextOptions = { /** diff --git a/packages/react-instantsearch/src/ui/RefinementList.tsx b/packages/react-instantsearch/src/ui/RefinementList.tsx index 95e7242fce4..a83a4d9f741 100644 --- a/packages/react-instantsearch/src/ui/RefinementList.tsx +++ b/packages/react-instantsearch/src/ui/RefinementList.tsx @@ -1,12 +1,12 @@ +import { getHighlightedParts, unescape } from 'instantsearch-core'; import { cx } from 'instantsearch-ui-components'; -import { getHighlightedParts, unescape } from 'instantsearch.js/es/lib/utils'; import React from 'react'; import { Highlight } from './Highlight'; import { ShowMoreButton } from './ShowMoreButton'; import type { ShowMoreButtonTranslations } from './ShowMoreButton'; -import type { RefinementListItem } from 'instantsearch.js/es/connectors/refinement-list/connectRefinementList'; +import type { RefinementListItem } from 'instantsearch-core'; export type RefinementListProps = React.ComponentProps<'div'> & { canRefine: boolean; diff --git a/packages/react-instantsearch/src/ui/__tests__/InfiniteHits.test.tsx b/packages/react-instantsearch/src/ui/__tests__/InfiniteHits.test.tsx index e8b433abf40..d0908c3654e 100644 --- a/packages/react-instantsearch/src/ui/__tests__/InfiniteHits.test.tsx +++ b/packages/react-instantsearch/src/ui/__tests__/InfiniteHits.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { InfiniteHits } from '../InfiniteHits'; import type { InfiniteHitsProps } from '../InfiniteHits'; -import type { Hit } from 'instantsearch.js'; +import type { Hit } from 'instantsearch-core'; describe('InfiniteHits', () => { function createProps( diff --git a/packages/react-instantsearch/src/widgets/Autocomplete.tsx b/packages/react-instantsearch/src/widgets/Autocomplete.tsx index 38d6d737756..ffff4030ebb 100644 --- a/packages/react-instantsearch/src/widgets/Autocomplete.tsx +++ b/packages/react-instantsearch/src/widgets/Autocomplete.tsx @@ -1,3 +1,10 @@ +import { + getPromptSuggestionHits, + isChatBusy, + isPromptSuggestion, + openChat, + warn, +} from 'instantsearch-core'; import { createAutocompleteComponent, createAutocompleteDetachedContainerComponent, @@ -12,11 +19,7 @@ import { createAutocompleteRecentSearchComponent, createAutocompleteStorage, cx, - getPromptSuggestionHits, - isPromptSuggestion, } from 'instantsearch-ui-components'; -import { isChatBusy, openChat } from 'instantsearch.js/es/lib/chat'; -import { warn } from 'instantsearch.js/es/lib/utils'; import React, { createElement, Fragment, @@ -42,6 +45,9 @@ import { Highlight } from './Highlight'; import { ReverseHighlight } from './ReverseHighlight'; import type { PlainSearchParameters } from 'algoliasearch-helper'; +import type { BaseHit, Hit, IndexUiState } from 'instantsearch-core'; +import type { TransformItemsIndicesConfig } from 'instantsearch-core'; +import type { ChatRenderState } from 'instantsearch-core'; import type { AutocompleteIndexClassNames, AutocompleteIndexConfig, @@ -49,9 +55,6 @@ import type { AutocompleteClassNames, AutocompleteIndexProps, } from 'instantsearch-ui-components'; -import type { BaseHit, Hit, IndexUiState } from 'instantsearch.js'; -import type { TransformItemsIndicesConfig } from 'instantsearch.js/es/connectors/autocomplete/connectAutocomplete'; -import type { ChatRenderState } from 'instantsearch.js/es/connectors/chat/connectChat'; import type { ComponentProps } from 'react'; const Autocomplete = createAutocompleteComponent({ diff --git a/packages/react-instantsearch/src/widgets/Chat.tsx b/packages/react-instantsearch/src/widgets/Chat.tsx index 62b1e022ea2..242e67d2e1e 100644 --- a/packages/react-instantsearch/src/widgets/Chat.tsx +++ b/packages/react-instantsearch/src/widgets/Chat.tsx @@ -1,4 +1,3 @@ -import { createChatComponent } from 'instantsearch-ui-components'; import { SearchIndexToolType, RecommendToolType, @@ -6,7 +5,8 @@ import { MemorySearchToolType, PonderToolType, DisplayResultsToolType, -} from 'instantsearch.js/es/lib/chat'; +} from 'instantsearch-core'; +import { createChatComponent } from 'instantsearch-ui-components'; import React, { createElement, Fragment, @@ -33,18 +33,20 @@ export { DisplayResultsToolType, }; +import type { + IndexUiState, + UIMessage, + UserClientSideTool, + UserClientSideTools, +} from 'instantsearch-core'; import type { Pragma, ChatProps as ChatUiProps, ChatLayoutOwnProps, RecommendComponentProps, RecordWithObjectID, - UserClientSideTool, - UserClientSideTools, ChatMessageProps, } from 'instantsearch-ui-components'; -import type { IndexUiState } from 'instantsearch.js'; -import type { UIMessage } from 'instantsearch.js/es/lib/chat'; import type { UseChatProps } from 'react-instantsearch-core'; const ChatUiComponent = createChatComponent({ diff --git a/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx b/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx index b089f83efc7..7c6367a9d2b 100644 --- a/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx +++ b/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx @@ -5,11 +5,11 @@ import { useInstantSearch, } from 'react-instantsearch-core'; +import type { Hit, BaseHit } from 'instantsearch-core'; import type { FrequentlyBoughtTogetherProps as FrequentlyBoughtTogetherPropsUiComponentProps, Pragma, } from 'instantsearch-ui-components'; -import type { Hit, BaseHit } from 'instantsearch.js'; import type { UseFrequentlyBoughtTogetherProps } from 'react-instantsearch-core'; type UiProps = Pick< diff --git a/packages/react-instantsearch/src/widgets/Highlight.tsx b/packages/react-instantsearch/src/widgets/Highlight.tsx index 48cefa9214b..807ff8c1e2b 100644 --- a/packages/react-instantsearch/src/widgets/Highlight.tsx +++ b/packages/react-instantsearch/src/widgets/Highlight.tsx @@ -2,14 +2,14 @@ import { getHighlightedParts, getPropertyByPath, unescape, -} from 'instantsearch.js/es/lib/utils'; +} from 'instantsearch-core'; import React from 'react'; import { Highlight as HighlightUiComponent } from '../ui/Highlight'; import type { PartialKeys } from '../types'; import type { HighlightProps as HighlightUiComponentProps } from '../ui/Highlight'; -import type { BaseHit, Hit } from 'instantsearch.js'; +import type { BaseHit, Hit } from 'instantsearch-core'; export type HighlightProps> = { hit: THit; diff --git a/packages/react-instantsearch/src/widgets/Hits.tsx b/packages/react-instantsearch/src/widgets/Hits.tsx index a33b973b0a5..5c2b5cc8ab0 100644 --- a/packages/react-instantsearch/src/widgets/Hits.tsx +++ b/packages/react-instantsearch/src/widgets/Hits.tsx @@ -2,12 +2,12 @@ import { createHitsComponent } from 'instantsearch-ui-components'; import React, { createElement, Fragment, useMemo } from 'react'; import { useHits } from 'react-instantsearch-core'; +import type { Hit, BaseHit } from 'instantsearch-core'; +import type { SendEventForHits } from 'instantsearch-core'; import type { HitsProps as HitsUiComponentProps, Pragma, } from 'instantsearch-ui-components'; -import type { Hit, BaseHit } from 'instantsearch.js'; -import type { SendEventForHits } from 'instantsearch.js/es/lib/utils'; import type { UseHitsProps } from 'react-instantsearch-core'; type UiProps = Pick< diff --git a/packages/react-instantsearch/src/widgets/InfiniteHits.tsx b/packages/react-instantsearch/src/widgets/InfiniteHits.tsx index e03b3c06520..ecdd0a62388 100644 --- a/packages/react-instantsearch/src/widgets/InfiniteHits.tsx +++ b/packages/react-instantsearch/src/widgets/InfiniteHits.tsx @@ -4,7 +4,7 @@ import { useInfiniteHits } from 'react-instantsearch-core'; import { InfiniteHits as InfiniteHitsUiComponent } from '../ui/InfiniteHits'; import type { InfiniteHitsProps as InfiniteHitsUiComponentProps } from '../ui/InfiniteHits'; -import type { BaseHit, Hit } from 'instantsearch.js'; +import type { BaseHit, Hit } from 'instantsearch-core'; import type { UseInfiniteHitsProps } from 'react-instantsearch-core'; type UiProps = Pick< diff --git a/packages/react-instantsearch/src/widgets/LookingSimilar.tsx b/packages/react-instantsearch/src/widgets/LookingSimilar.tsx index 16174432fcd..d0a5f5bccb9 100644 --- a/packages/react-instantsearch/src/widgets/LookingSimilar.tsx +++ b/packages/react-instantsearch/src/widgets/LookingSimilar.tsx @@ -2,11 +2,11 @@ import { createLookingSimilarComponent } from 'instantsearch-ui-components'; import React, { createElement, Fragment, useMemo } from 'react'; import { useLookingSimilar, useInstantSearch } from 'react-instantsearch-core'; +import type { Hit, BaseHit } from 'instantsearch-core'; import type { LookingSimilarProps as LookingSimilarPropsUiComponentProps, Pragma, } from 'instantsearch-ui-components'; -import type { Hit, BaseHit } from 'instantsearch.js'; import type { UseLookingSimilarProps } from 'react-instantsearch-core'; type UiProps = Pick< diff --git a/packages/react-instantsearch/src/widgets/RefinementList.tsx b/packages/react-instantsearch/src/widgets/RefinementList.tsx index 718189d0b92..d9bb3dfcdf5 100644 --- a/packages/react-instantsearch/src/widgets/RefinementList.tsx +++ b/packages/react-instantsearch/src/widgets/RefinementList.tsx @@ -6,8 +6,7 @@ import { SearchBox as SearchBoxUiComponent } from '../ui/SearchBox'; import type { RefinementListProps as RefinementListUiComponentProps } from '../ui/RefinementList'; import type { SearchBoxProps } from '../ui/SearchBox'; -import type { RefinementListItem } from 'instantsearch.js/es/connectors/refinement-list/connectRefinementList'; -import type { RefinementListWidgetParams } from 'instantsearch.js/es/widgets/refinement-list/refinement-list'; +import type { RefinementListItem } from 'instantsearch-core'; import type { UseRefinementListProps } from 'react-instantsearch-core'; type UiProps = Pick< @@ -28,11 +27,11 @@ export type RefinementListProps = Omit< RefinementListUiComponentProps, keyof UiProps > & - UseRefinementListProps & - Pick< - RefinementListWidgetParams, - 'searchable' | 'searchablePlaceholder' | 'searchableSelectOnSubmit' - > & { + UseRefinementListProps & { + searchable?: boolean; + searchablePlaceholder?: string; + searchableSelectOnSubmit?: boolean; + } & { translations?: Partial< UiProps['translations'] & { submitButtonTitle: string; diff --git a/packages/react-instantsearch/src/widgets/RelatedProducts.tsx b/packages/react-instantsearch/src/widgets/RelatedProducts.tsx index 009714ad128..05ab24abee1 100644 --- a/packages/react-instantsearch/src/widgets/RelatedProducts.tsx +++ b/packages/react-instantsearch/src/widgets/RelatedProducts.tsx @@ -2,11 +2,11 @@ import { createRelatedProductsComponent } from 'instantsearch-ui-components'; import React, { createElement, Fragment, useMemo } from 'react'; import { useInstantSearch, useRelatedProducts } from 'react-instantsearch-core'; +import type { BaseHit, Hit } from 'instantsearch-core'; import type { RelatedProductsProps as RelatedProductsUiComponentProps, Pragma, } from 'instantsearch-ui-components'; -import type { BaseHit, Hit } from 'instantsearch.js'; import type { UseRelatedProductsProps } from 'react-instantsearch-core'; type UiProps = Pick< diff --git a/packages/react-instantsearch/src/widgets/ReverseHighlight.tsx b/packages/react-instantsearch/src/widgets/ReverseHighlight.tsx index fb88ceacf42..54e99380498 100644 --- a/packages/react-instantsearch/src/widgets/ReverseHighlight.tsx +++ b/packages/react-instantsearch/src/widgets/ReverseHighlight.tsx @@ -2,14 +2,14 @@ import { getHighlightedParts, getPropertyByPath, unescape, -} from 'instantsearch.js/es/lib/utils'; +} from 'instantsearch-core'; import React from 'react'; import { ReverseHighlight as ReverseHighlightUiComponent } from '../ui/ReverseHighlight'; import type { PartialKeys } from '../types'; import type { ReverseHighlightProps as ReverseHighlightUiComponentProps } from '../ui/ReverseHighlight'; -import type { BaseHit, Hit } from 'instantsearch.js'; +import type { BaseHit, Hit } from 'instantsearch-core'; export type ReverseHighlightProps> = { hit: THit; diff --git a/packages/react-instantsearch/src/widgets/SearchBox.tsx b/packages/react-instantsearch/src/widgets/SearchBox.tsx index 2a1728946ee..a6a094c03c1 100644 --- a/packages/react-instantsearch/src/widgets/SearchBox.tsx +++ b/packages/react-instantsearch/src/widgets/SearchBox.tsx @@ -1,11 +1,11 @@ -import { isChatBusy, openChat } from 'instantsearch.js/es/lib/chat'; +import { isChatBusy, openChat } from 'instantsearch-core'; import React, { useRef, useState } from 'react'; import { useInstantSearch, useSearchBox } from 'react-instantsearch-core'; import { SearchBox as SearchBoxUiComponent } from '../ui/SearchBox'; import type { SearchBoxProps as SearchBoxUiComponentProps } from '../ui/SearchBox'; -import type { ChatRenderState } from 'instantsearch.js/es/connectors/chat/connectChat'; +import type { ChatRenderState } from 'instantsearch-core'; import type { UseSearchBoxProps } from 'react-instantsearch-core'; type UiProps = Pick< diff --git a/packages/react-instantsearch/src/widgets/Snippet.tsx b/packages/react-instantsearch/src/widgets/Snippet.tsx index d583448e22a..f227801d88c 100644 --- a/packages/react-instantsearch/src/widgets/Snippet.tsx +++ b/packages/react-instantsearch/src/widgets/Snippet.tsx @@ -2,14 +2,14 @@ import { getHighlightedParts, getPropertyByPath, unescape, -} from 'instantsearch.js/es/lib/utils'; +} from 'instantsearch-core'; import React from 'react'; import { Snippet as SnippetUiComponent } from '../ui/Snippet'; import type { PartialKeys } from '../types'; import type { SnippetProps as SnippetUiComponentProps } from '../ui/Snippet'; -import type { BaseHit, Hit } from 'instantsearch.js'; +import type { BaseHit, Hit } from 'instantsearch-core'; export type SnippetProps> = { hit: THit; diff --git a/packages/react-instantsearch/src/widgets/TrendingItems.tsx b/packages/react-instantsearch/src/widgets/TrendingItems.tsx index 8fc23a4411e..518ac76c587 100644 --- a/packages/react-instantsearch/src/widgets/TrendingItems.tsx +++ b/packages/react-instantsearch/src/widgets/TrendingItems.tsx @@ -2,11 +2,11 @@ import { createTrendingItemsComponent } from 'instantsearch-ui-components'; import React, { createElement, Fragment, useMemo } from 'react'; import { useInstantSearch, useTrendingItems } from 'react-instantsearch-core'; +import type { BaseHit, Hit } from 'instantsearch-core'; import type { TrendingItemsProps as TrendingItemsUiComponentProps, Pragma, } from 'instantsearch-ui-components'; -import type { BaseHit, Hit } from 'instantsearch.js'; import type { UseTrendingItemsProps } from 'react-instantsearch-core'; type UiProps = Pick< diff --git a/packages/react-instantsearch/src/widgets/__tests__/Hits.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/Hits.test.tsx index 9376ea8928e..bbac90d3346 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/Hits.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/Hits.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { Hits } from '../Hits'; import type { MockSearchClient } from '@instantsearch/mocks'; -import type { AlgoliaHit } from 'instantsearch.js'; +import type { AlgoliaHit } from 'instantsearch-core'; type CustomRecord = { somethingSpecial: string; diff --git a/packages/react-instantsearch/src/widgets/__tests__/InfiniteHits.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/InfiniteHits.test.tsx index f0c9e88459d..4f0bde1188d 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/InfiniteHits.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/InfiniteHits.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { InfiniteHits } from '../InfiniteHits'; import type { MockSearchClient } from '@instantsearch/mocks'; -import type { AlgoliaHit } from 'instantsearch.js'; +import type { AlgoliaHit } from 'instantsearch-core'; type CustomHit = { somethingSpecial: string }; diff --git a/packages/react-instantsearch/src/widgets/__tests__/Pagination.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/Pagination.test.tsx index ee0d1471b4a..72375fb2d8c 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/Pagination.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/Pagination.test.tsx @@ -15,7 +15,7 @@ import React from 'react'; import { Pagination } from '../Pagination'; import type { MockSearchClient } from '@instantsearch/mocks'; -import type { SearchClient } from 'instantsearch.js'; +import type { SearchClient } from 'instantsearch-core'; function createMockedSearchClient({ nbPages }: { nbPages?: number } = {}) { return createAlgoliaSearchClient({ diff --git a/packages/react-instantsearch/src/widgets/__tests__/SearchBox.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/SearchBox.test.tsx index 2d5ba2e3201..64f7c03dc6c 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/SearchBox.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/SearchBox.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { SearchBox } from '../SearchBox'; -import type { UiState } from 'instantsearch.js'; +import type { UiState } from 'instantsearch-core'; describe('SearchBox', () => { test('forwards custom class names and `div` props to the root element', () => { diff --git a/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx b/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx index 49c97a76d2d..dc643fdc122 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx @@ -8,7 +8,7 @@ import { import * as widgets from '../..'; -import type { InstantSearch as InstantSearchClass } from 'instantsearch.js'; +import type { InstantSearch as InstantSearchClass } from 'instantsearch-core'; import type { ComponentProps } from 'react'; // We only track widgets that use connectors. diff --git a/packages/react-instantsearch/src/widgets/chat/tools/DisplayResultsTool.tsx b/packages/react-instantsearch/src/widgets/chat/tools/DisplayResultsTool.tsx index 48f1b8ce5c2..62ce6199114 100644 --- a/packages/react-instantsearch/src/widgets/chat/tools/DisplayResultsTool.tsx +++ b/packages/react-instantsearch/src/widgets/chat/tools/DisplayResultsTool.tsx @@ -10,10 +10,12 @@ import { Carousel } from '../../../components'; import type { ClientSideToolComponentProps, + UserClientSideTool, +} from 'instantsearch-core'; +import type { Pragma, RecommendComponentProps, RecordWithObjectID, - UserClientSideTool, } from 'instantsearch-ui-components'; type ItemComponent = RecommendComponentProps['itemComponent']; diff --git a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx index 4df2cd5538f..753d0ea7e69 100644 --- a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx +++ b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx @@ -1,3 +1,7 @@ +import { + addAbsolutePosition, + addQueryID, +} from 'instantsearch-core'; import { ChevronLeftIcon, ChevronRightIcon, @@ -5,7 +9,6 @@ import { createButtonComponent, getFacetFiltersFromToolInput, } from 'instantsearch-ui-components'; -import { addAbsolutePosition, addQueryID } from 'instantsearch.js/es/lib/utils'; import React, { createElement } from 'react'; import { Carousel } from '../../../components'; @@ -13,11 +16,13 @@ import { Carousel } from '../../../components'; import type { SearchParameters } from 'algoliasearch-helper'; import type { ClientSideToolComponentProps, + SearchToolInput, + UserClientSideTool, +} from 'instantsearch-core'; +import type { Pragma, RecommendComponentProps, RecordWithObjectID, - SearchToolInput, - UserClientSideTool, } from 'instantsearch-ui-components'; import type { ComponentProps } from 'react'; diff --git a/packages/react-instantsearch/src/widgets/chat/tools/__tests__/DisplayResultsTool.test.tsx b/packages/react-instantsearch/src/widgets/chat/tools/__tests__/DisplayResultsTool.test.tsx index bb670d62872..53f836b5c9c 100644 --- a/packages/react-instantsearch/src/widgets/chat/tools/__tests__/DisplayResultsTool.test.tsx +++ b/packages/react-instantsearch/src/widgets/chat/tools/__tests__/DisplayResultsTool.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { createDisplayResultsTool } from '../DisplayResultsTool'; -import type { ClientSideToolComponentProps } from 'instantsearch-ui-components'; +import type { ClientSideToolComponentProps } from 'instantsearch-core'; type TestResult = { objectID: string; diff --git a/packages/react-instantsearch/src/widgets/chat/tools/__tests__/SearchIndexTool.test.tsx b/packages/react-instantsearch/src/widgets/chat/tools/__tests__/SearchIndexTool.test.tsx index dd521532a53..6356431d8bf 100644 --- a/packages/react-instantsearch/src/widgets/chat/tools/__tests__/SearchIndexTool.test.tsx +++ b/packages/react-instantsearch/src/widgets/chat/tools/__tests__/SearchIndexTool.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { createCarouselTool } from '../SearchIndexTool'; -import type { ClientSideToolComponentProps } from 'instantsearch-ui-components'; +import type { ClientSideToolComponentProps } from 'instantsearch-core'; type TestHit = { objectID: string; diff --git a/packages/vue-instantsearch/__mocks__/instantsearch.js/es.js b/packages/vue-instantsearch/__mocks__/instantsearch-core.js similarity index 91% rename from packages/vue-instantsearch/__mocks__/instantsearch.js/es.js rename to packages/vue-instantsearch/__mocks__/instantsearch-core.js index d844b8782ac..f4037f4fba4 100644 --- a/packages/vue-instantsearch/__mocks__/instantsearch.js/es.js +++ b/packages/vue-instantsearch/__mocks__/instantsearch-core.js @@ -1,6 +1,8 @@ /* eslint-disable import/no-commonjs */ const isPlainObject = require('lodash/isPlainObject'); +const actual = jest.requireActual('instantsearch-core'); + class RoutingManager { constructor(routing) { this._routing = routing; @@ -57,7 +59,7 @@ const fakeInstantSearch = jest.fn( instantsearchInstance.mainIndex.addWidgets(widgets); }, removeWidgets(widgets) { - widgets.forEach(widget => { + widgets.forEach((widget) => { const i = instantsearchInstance.mainIndex._widgets.indexOf(widget); if (i === -1) { return; @@ -71,4 +73,7 @@ const fakeInstantSearch = jest.fn( } ); -module.exports = fakeInstantSearch; +module.exports = { + ...actual, + instantsearch: fakeInstantSearch, +}; diff --git a/packages/vue-instantsearch/package.json b/packages/vue-instantsearch/package.json index 9a8f085ca07..7183187837a 100644 --- a/packages/vue-instantsearch/package.json +++ b/packages/vue-instantsearch/package.json @@ -38,16 +38,20 @@ "dependencies": { "@swc/helpers": "0.5.18", "instantsearch-ui-components": "0.32.0", - "instantsearch.js": "4.103.0", - "mitt": "^2.1.0" + "mitt": "^2.1.0", + "instantsearch-core": "0.1.0" }, "peerDependencies": { "@vue/server-renderer": "^3.1.2", "algoliasearch": ">= 3.32.0 < 6", + "instantsearch.js": ">= 4.96.2 < 5", "vue": "^2.6.0 || >=3.0.0-rc.0", "vue-server-renderer": "^2.6.11" }, "peerDependenciesMeta": { + "instantsearch.js": { + "optional": true + }, "vue-server-renderer": { "optional": true }, diff --git a/packages/vue-instantsearch/rollup.config.mjs b/packages/vue-instantsearch/rollup.config.mjs index 9117e4c8007..8deb282487f 100644 --- a/packages/vue-instantsearch/rollup.config.mjs +++ b/packages/vue-instantsearch/rollup.config.mjs @@ -139,6 +139,7 @@ const external = id => [ 'algoliasearch-helper', 'instantsearch.js', + 'instantsearch-core', 'instantsearch-ui-components', 'vue', 'mitt', diff --git a/packages/vue-instantsearch/src/__tests__/common-connectors.test.js b/packages/vue-instantsearch/src/__tests__/common-connectors.test.js index aa40143b4fd..5cf64372627 100644 --- a/packages/vue-instantsearch/src/__tests__/common-connectors.test.js +++ b/packages/vue-instantsearch/src/__tests__/common-connectors.test.js @@ -14,7 +14,7 @@ import { connectRatingMenu, connectRefinementList, connectToggleRefinement, -} from 'instantsearch.js/es/connectors/index'; +} from 'instantsearch-core'; import { nextTick, mountApp } from '../../test/utils'; import { @@ -24,7 +24,7 @@ import { createWidgetMixin, } from '../instantsearch'; import { renderCompat } from '../util/vue-compat'; -jest.unmock('instantsearch.js/es'); +jest.unmock('instantsearch-core'); const testSetups = { async createRefinementListConnectorTests({ diff --git a/packages/vue-instantsearch/src/__tests__/common-shared-composition.test.js b/packages/vue-instantsearch/src/__tests__/common-shared-composition.test.js index 462e4d78472..044aeb533e3 100644 --- a/packages/vue-instantsearch/src/__tests__/common-shared-composition.test.js +++ b/packages/vue-instantsearch/src/__tests__/common-shared-composition.test.js @@ -7,7 +7,7 @@ import * as testSuites from '@instantsearch/tests/shared-composition'; import { nextTick, mountApp } from '../../test/utils'; import { AisInstantSearch, AisRefinementList } from '../instantsearch'; import { renderCompat } from '../util/vue-compat'; -jest.unmock('instantsearch.js/es'); +jest.unmock('instantsearch-core'); const testSetups = { async createSharedCompositionTests({ instantSearchOptions, widgetParams }) { diff --git a/packages/vue-instantsearch/src/__tests__/common-shared.test.js b/packages/vue-instantsearch/src/__tests__/common-shared.test.js index b3f07a13ee4..e62bd8afbb4 100644 --- a/packages/vue-instantsearch/src/__tests__/common-shared.test.js +++ b/packages/vue-instantsearch/src/__tests__/common-shared.test.js @@ -6,7 +6,7 @@ import * as testSuites from '@instantsearch/tests/shared'; import { connectMenu, connectPagination, -} from 'instantsearch.js/es/connectors/index'; +} from 'instantsearch-core'; import { nextTick, mountApp } from '../../test/utils'; import { @@ -19,7 +19,7 @@ import { createWidgetMixin, } from '../instantsearch'; import { renderCompat } from '../util/vue-compat'; -jest.unmock('instantsearch.js/es'); +jest.unmock('instantsearch-core'); const testSetups = { async createSharedTests({ instantSearchOptions, widgetParams }) { diff --git a/packages/vue-instantsearch/src/__tests__/common-widgets.test.js b/packages/vue-instantsearch/src/__tests__/common-widgets.test.js index 2bd26faa7e9..a2fca2378e6 100644 --- a/packages/vue-instantsearch/src/__tests__/common-widgets.test.js +++ b/packages/vue-instantsearch/src/__tests__/common-widgets.test.js @@ -32,7 +32,7 @@ import { } from '../instantsearch'; import { renderCompat } from '../util/vue-compat'; -jest.unmock('instantsearch.js/es'); +jest.unmock('instantsearch-core'); /** * prevent rethrowing InstantSearch errors, so tests can be asserted. @@ -330,8 +330,8 @@ const testSetups = { return { algoliaAgents: [ - `instantsearch.js (${ - require('../../../instantsearch.js/package.json').version + `instantsearch-core (${ + require('../../../instantsearch-core/package.json').version })`, `Vue InstantSearch (${ require('../../../vue-instantsearch/package.json').version diff --git a/packages/vue-instantsearch/src/components/Autocomplete.vue b/packages/vue-instantsearch/src/components/Autocomplete.vue index 11f025ccdd5..fcbcd52ff6f 100644 --- a/packages/vue-instantsearch/src/components/Autocomplete.vue +++ b/packages/vue-instantsearch/src/components/Autocomplete.vue @@ -20,7 +20,7 @@