From 89dd7ae4d45dea4dafbe451e55b01f2e45caf07e Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Fri, 31 May 2024 22:02:17 +0200 Subject: [PATCH] feat(typescript): allow generics for the Hit type in all hit-displaying connectors and widgets (#6218) * POC: make hits connector and widget (js) generic to work around, `satisfies` operator gets confused with the generic type of widgetParams being different to the ones passed to the connector itself. Maybe there's a smart way to pass the generics around though * refactor: simplify and not allow generic for params twice this ensures no extra params are allowed unless declared in the connector in advance * use in examples * chore: remove comment * fix: allow WidgetRenderState generic by retyping * chore: change type things --- bundlesize.config.json | 6 +- .../js/e-commerce-umd/src/widgets/Products.ts | 11 +- .../js/e-commerce/src/widgets/Products.ts | 11 +- package.json | 2 +- .../.storybook/decorators/withLifecycle.ts | 3 +- .../scripts/typescript/api-extractor.json | 5 + .../__tests__/InfiniteHits.test.tsx | 2 +- .../connectFrequentlyBoughtTogether-test.ts | 21 +- .../connectFrequentlyBoughtTogether.ts | 218 ++++++++------- .../__tests__/connectGeoSearch-test.ts | 136 ++++----- .../connectors/geo-search/connectGeoSearch.ts | 53 ++-- .../hits/__tests__/connectHits-test.ts | 60 ++-- .../__tests__/connectHitsWithInsights-test.ts | 10 +- .../src/connectors/hits/connectHits.ts | 48 ++-- .../__tests__/connectInfiniteHits-test.ts | 112 ++++---- .../connectInfiniteHitsWithInsights-test.ts | 10 +- .../infinite-hits/connectInfiniteHits.ts | 68 +++-- .../__tests__/connectLookingSimilar-test.ts | 135 +++++++++ .../looking-similar/connectLookingSimilar.ts | 225 ++++++++------- .../__tests__/connectQueryRules-test.ts | 2 +- .../__tests__/connectRelatedProducts-test.ts | 135 +++++++++ .../connectRelatedProducts.ts | 234 +++++++++------- .../sort-by/__tests__/connectSortBy-test.ts | 2 +- .../__tests__/connectTrendingItems-test.ts | 173 ++++++++++++ .../trending-items/connectTrendingItems.ts | 240 +++++++++------- .../instantsearch.js/src/lib/InstantSearch.ts | 4 +- .../src/lib/__tests__/InstantSearch-test.tsx | 3 +- .../__tests__/resolveSearchParameters-test.ts | 2 +- .../src/lib/utils/checkIndexUiState.ts | 3 +- .../src/lib/utils/checkRendering.ts | 6 +- .../src/lib/utils/getRefinements.ts | 2 +- .../src/lib/utils/getWidgetAttribute.ts | 3 +- .../src/lib/utils/isIndexWidget.ts | 3 +- .../src/lib/utils/render-args.ts | 3 +- .../src/lib/utils/resolveSearchParameters.ts | 2 +- .../src/lib/utils/setIndexHelperState.ts | 3 +- .../middlewares/createMetadataMiddleware.ts | 8 +- packages/instantsearch.js/src/types/index.ts | 3 - .../instantsearch.js/src/types/results.ts | 54 ++-- packages/instantsearch.js/src/types/widget.ts | 4 +- .../frequently-bought-together.tsx | 114 ++++---- .../geo-search/__tests__/geo-search-test.ts | 132 ++++----- .../src/widgets/geo-search/geo-search.ts | 28 +- .../src/widgets/hits/defaultTemplates.ts | 6 +- .../src/widgets/hits/hits.tsx | 81 +++--- .../instantsearch.js/src/widgets/index.ts | 1 + .../__tests__/infinite-hits-test.ts | 44 +-- .../widgets/infinite-hits/infinite-hits.tsx | 82 +++--- .../looking-similar/looking-similar.tsx | 77 ++--- .../related-products/related-products.tsx | 48 ++-- .../widgets/trending-items/trending-items.tsx | 85 +++--- .../test/createInstantSearch.ts | 2 +- .../src/connectors/useGeoSearch.ts | 7 +- yarn.lock | 262 +++++++++--------- 54 files changed, 1851 insertions(+), 1143 deletions(-) create mode 100644 packages/instantsearch.js/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts create mode 100644 packages/instantsearch.js/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts create mode 100644 packages/instantsearch.js/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts diff --git a/bundlesize.config.json b/bundlesize.config.json index 3e0217db58..98d6daeab8 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -10,11 +10,11 @@ }, { "path": "./packages/instantsearch.js/dist/instantsearch.production.min.js", - "maxSize": "81.5 kB" + "maxSize": "83 kB" }, { "path": "./packages/instantsearch.js/dist/instantsearch.development.js", - "maxSize": "177.5 kB" + "maxSize": "180 kB" }, { "path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js", @@ -22,7 +22,7 @@ }, { "path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js", - "maxSize": "63.25 kB" + "maxSize": "64 kB" }, { "path": "packages/vue-instantsearch/vue2/umd/index.js", diff --git a/examples/js/e-commerce-umd/src/widgets/Products.ts b/examples/js/e-commerce-umd/src/widgets/Products.ts index b5a2bc0c64..6561fd9e9a 100644 --- a/examples/js/e-commerce-umd/src/widgets/Products.ts +++ b/examples/js/e-commerce-umd/src/widgets/Products.ts @@ -1,6 +1,15 @@ const { hits } = window.instantsearch.widgets; -export const products = hits({ +type Hit = { + name: string; + image: string; + categories: string[]; + description: string; + price: number; + rating: number; +}; + +export const products = hits({ container: '[data-widget="hits"]', templates: { item(hit, { html, components }) { diff --git a/examples/js/e-commerce/src/widgets/Products.ts b/examples/js/e-commerce/src/widgets/Products.ts index 02c3f6c5c6..d90d7eae64 100644 --- a/examples/js/e-commerce/src/widgets/Products.ts +++ b/examples/js/e-commerce/src/widgets/Products.ts @@ -1,6 +1,15 @@ import { hits } from 'instantsearch.js/es/widgets'; -export const products = hits({ +type Hit = { + name: string; + image: string; + categories: string[]; + description: string; + price: number; + rating: number; +}; + +export const products = hits({ container: '[data-widget="hits"]', templates: { item(hit, { html, components }) { diff --git a/package.json b/package.json index c4f8b69e3a..8496cee91d 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@babel/preset-react": "7.14.5", "@babel/preset-typescript": "7.15.0", "@googlemaps/jest-mocks": "2.7.5", - "@microsoft/api-extractor": "7.18.0", + "@microsoft/api-extractor": "7.45.1", "@storybook/addon-actions": "5.3.9", "@storybook/addon-knobs": "5.3.9", "@storybook/addon-options": "5.3.9", diff --git a/packages/instantsearch.js/.storybook/decorators/withLifecycle.ts b/packages/instantsearch.js/.storybook/decorators/withLifecycle.ts index f8a8eeadec..013661c5a3 100644 --- a/packages/instantsearch.js/.storybook/decorators/withLifecycle.ts +++ b/packages/instantsearch.js/.storybook/decorators/withLifecycle.ts @@ -1,5 +1,4 @@ -import { InstantSearch, Widget } from '../../src/types'; -import { IndexWidget } from '../../src/widgets/index/index'; +import { InstantSearch, Widget, IndexWidget } from '../../src/types'; const setDisabledState = (element: HTMLButtonElement, state: boolean) => { element.disabled = state; diff --git a/packages/instantsearch.js/scripts/typescript/api-extractor.json b/packages/instantsearch.js/scripts/typescript/api-extractor.json index 5d6ac71136..6de6ed2bcd 100644 --- a/packages/instantsearch.js/scripts/typescript/api-extractor.json +++ b/packages/instantsearch.js/scripts/typescript/api-extractor.json @@ -41,6 +41,11 @@ "ae-missing-release-tag": { "logLevel": "none" + }, + + "ae-wrong-input-file-type": { + // This may be returned falsely, to investigate! + "logLevel": "warning" } }, diff --git a/packages/instantsearch.js/src/components/InfiniteHits/__tests__/InfiniteHits.test.tsx b/packages/instantsearch.js/src/components/InfiniteHits/__tests__/InfiniteHits.test.tsx index d3f7b456c2..3b405876e3 100644 --- a/packages/instantsearch.js/src/components/InfiniteHits/__tests__/InfiniteHits.test.tsx +++ b/packages/instantsearch.js/src/components/InfiniteHits/__tests__/InfiniteHits.test.tsx @@ -11,8 +11,8 @@ import { h } from 'preact'; import { prepareTemplateProps } from '../../../lib/templating'; import InfiniteHits from '../InfiniteHits'; +import type { Hit } from '../../../types'; import type { InfiniteHitsProps } from '../InfiniteHits'; -import type { Hit } from 'instantsearch.js'; beforeEach(() => { document.body.innerHTML = ''; diff --git a/packages/instantsearch.js/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts b/packages/instantsearch.js/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts index 7f8ee35850..748c998bbf 100644 --- a/packages/instantsearch.js/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts +++ b/packages/instantsearch.js/src/connectors/frequently-bought-together/__tests__/connectFrequentlyBoughtTogether-test.ts @@ -40,6 +40,21 @@ describe('connectFrequentlyBoughtTogether', () => { ); }); + it('accepts custom parameters', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customFrequentlyBoughtTogether = connectFrequentlyBoughtTogether<{ + container: string; + }>(render, unmount); + const widget = customFrequentlyBoughtTogether({ + container: '#container', + objectIDs: ['1'], + }); + + expect(widget.$$type).toBe('ais.frequentlyBoughtTogether'); + }); + it('Renders during init and render', () => { const renderFn = jest.fn(); const makeWidget = connectFrequentlyBoughtTogether(renderFn); @@ -51,7 +66,7 @@ describe('connectFrequentlyBoughtTogether', () => { const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -68,7 +83,7 @@ describe('connectFrequentlyBoughtTogether', () => { helper, }); - widget.render!(renderOptions); + widget.render(renderOptions); expect(renderFn).toHaveBeenCalledTimes(2); expect(renderFn).toHaveBeenLastCalledWith( @@ -90,7 +105,7 @@ describe('connectFrequentlyBoughtTogether', () => { }); // @ts-expect-error - const actual = widget.getWidgetParameters!(new RecommendParameters(), { + const actual = widget.getWidgetParameters(new RecommendParameters(), { uiState: {}, }); diff --git a/packages/instantsearch.js/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts b/packages/instantsearch.js/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts index c72c31c42f..4190ef67a9 100644 --- a/packages/instantsearch.js/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts +++ b/packages/instantsearch.js/src/connectors/frequently-bought-together/connectFrequentlyBoughtTogether.ts @@ -6,7 +6,15 @@ import { TAG_PLACEHOLDER, } from '../../lib/utils'; -import type { Connector, TransformItems, Hit, BaseHit } from '../../types'; +import type { + Connector, + TransformItems, + Hit, + BaseHit, + Renderer, + Unmounter, + UnknownWidgetParams, +} from '../../types'; import type { PlainSearchParameters, RecommendResultItem, @@ -18,7 +26,7 @@ const withUsage = createDocumentationMessageGenerator({ }); export type FrequentlyBoughtTogetherRenderState< - THit extends BaseHit = BaseHit + THit extends NonNullable = BaseHit > = { /** * The matched recommendations from Algolia API. @@ -27,7 +35,7 @@ export type FrequentlyBoughtTogetherRenderState< }; export type FrequentlyBoughtTogetherConnectorParams< - THit extends BaseHit = BaseHit + THit extends NonNullable = BaseHit > = { /** * The objectIDs of the items to get the frequently bought together items for. @@ -66,108 +74,116 @@ export type FrequentlyBoughtTogetherConnectorParams< }; export type FrequentlyBoughtTogetherWidgetDescription< - THit extends BaseHit = BaseHit + THit extends NonNullable = BaseHit > = { $$type: 'ais.frequentlyBoughtTogether'; renderState: FrequentlyBoughtTogetherRenderState; }; -export type FrequentlyBoughtTogetherConnector = - Connector< - FrequentlyBoughtTogetherWidgetDescription, - FrequentlyBoughtTogetherConnectorParams - >; - -const connectFrequentlyBoughtTogether: FrequentlyBoughtTogetherConnector = - function connectFrequentlyBoughtTogether(renderFn, unmountFn = noop) { - checkRendering(renderFn, withUsage()); - - return (widgetParams) => { - const { - // @MAJOR: this can default to false - escapeHTML = true, - transformItems = ((items) => items) as NonNullable< - FrequentlyBoughtTogetherConnectorParams['transformItems'] - >, - objectIDs, - limit, - threshold, - queryParameters, - } = widgetParams || {}; - - if (!objectIDs || objectIDs.length === 0) { - throw new Error(withUsage('The `objectIDs` option is required.')); - } - - return { - dependsOn: 'recommend', - $$type: 'ais.frequentlyBoughtTogether', - - init(initOptions) { - renderFn( - { - ...this.getWidgetRenderState(initOptions), - instantSearchInstance: initOptions.instantSearchInstance, - }, - true - ); - }, - - render(renderOptions) { - const renderState = this.getWidgetRenderState(renderOptions); - - renderFn( - { - ...renderState, - instantSearchInstance: renderOptions.instantSearchInstance, - }, - false - ); - }, - - getRenderState(renderState) { - return renderState; - }, - - getWidgetRenderState({ results }) { - if (results === null || results === undefined) { - return { items: [], widgetParams }; - } - - if (escapeHTML && results.hits.length > 0) { - results.hits = escapeHits(results.hits); - } - - const transformedItems = transformItems(results.hits, { - results: results as RecommendResultItem, - }); - - return { items: transformedItems, widgetParams }; - }, - - dispose({ recommendState }) { - unmountFn(); - return recommendState.removeParams(this.$$id!); - }, - - getWidgetParameters(state) { - return objectIDs.reduce( - (acc, objectID) => - acc.addFrequentlyBoughtTogether({ - objectID, - threshold, - maxRecommendations: limit, - queryParameters: { - ...queryParameters, - ...(escapeHTML ? TAG_PLACEHOLDER : {}), - }, - $$id: this.$$id!, - }), - state.removeParams(this.$$id!) - ); - }, - }; +export type FrequentlyBoughtTogetherConnector< + THit extends NonNullable = BaseHit +> = Connector< + FrequentlyBoughtTogetherWidgetDescription, + FrequentlyBoughtTogetherConnectorParams +>; + +export default (function connectFrequentlyBoughtTogether< + TWidgetParams extends UnknownWidgetParams +>( + renderFn: Renderer< + FrequentlyBoughtTogetherRenderState, + TWidgetParams & FrequentlyBoughtTogetherConnectorParams + >, + unmountFn: Unmounter = noop +) { + checkRendering(renderFn, withUsage()); + + return = BaseHit>( + widgetParams: TWidgetParams & FrequentlyBoughtTogetherConnectorParams + ) => { + const { + // @MAJOR: this can default to false + escapeHTML = true, + transformItems = ((items) => items) as NonNullable< + FrequentlyBoughtTogetherConnectorParams['transformItems'] + >, + objectIDs, + limit, + threshold, + queryParameters, + } = widgetParams || {}; + + if (!objectIDs || objectIDs.length === 0) { + throw new Error(withUsage('The `objectIDs` option is required.')); + } + + return { + dependsOn: 'recommend', + $$type: 'ais.frequentlyBoughtTogether', + + init(initOptions) { + renderFn( + { + ...this.getWidgetRenderState(initOptions), + instantSearchInstance: initOptions.instantSearchInstance, + }, + true + ); + }, + + render(renderOptions) { + const renderState = this.getWidgetRenderState(renderOptions); + + renderFn( + { + ...renderState, + instantSearchInstance: renderOptions.instantSearchInstance, + }, + false + ); + }, + + getRenderState(renderState) { + return renderState; + }, + + getWidgetRenderState({ results }) { + if (results === null || results === undefined) { + return { items: [], widgetParams }; + } + + if (escapeHTML && results.hits.length > 0) { + results.hits = escapeHits(results.hits); + } + + const transformedItems = transformItems(results.hits, { + results: results as RecommendResultItem, + }); + + return { items: transformedItems, widgetParams }; + }, + + dispose({ recommendState }) { + unmountFn(); + return recommendState.removeParams(this.$$id!); + }, + + getWidgetParameters(state) { + return objectIDs.reduce( + (acc, objectID) => + acc.addFrequentlyBoughtTogether({ + objectID, + threshold, + maxRecommendations: limit, + queryParameters: { + ...queryParameters, + ...(escapeHTML ? TAG_PLACEHOLDER : {}), + }, + $$id: this.$$id!, + }), + state.removeParams(this.$$id!) + ); + }, }; }; - -export default connectFrequentlyBoughtTogether; +} satisfies FrequentlyBoughtTogetherConnector); diff --git a/packages/instantsearch.js/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts b/packages/instantsearch.js/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts index 6163d4ca33..44e949d0bd 100644 --- a/packages/instantsearch.js/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts +++ b/packages/instantsearch.js/src/connectors/geo-search/__tests__/connectGeoSearch-test.ts @@ -37,7 +37,7 @@ describe('connectGeoSearch', () => { const helper = createFakeHelper(); - widget.init!(createInitOptions({ helper })); + widget.init(createInitOptions({ helper })); const { refine } = widget.getWidgetRenderState( createInitOptions({ helper }) @@ -76,7 +76,7 @@ describe('connectGeoSearch', () => { const instantSearchInstance = createInstantSearch(); const helper = instantSearchInstance.mainHelper!; - widget.init!( + widget.init( createInitOptions({ instantSearchInstance, }) @@ -88,7 +88,7 @@ describe('connectGeoSearch', () => { }), ]); - widget.render!( + widget.render( createRenderOptions({ instantSearchInstance, results, @@ -141,6 +141,18 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }); }); + it('accepts custom parameters', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customGeoSearch = connectGeoSearch<{ + container: string; + }>(render, unmount); + const widget = customGeoSearch({ container: '#container' }); + + expect(widget.$$type).toBe('ais.geoSearch'); + }); + it('expect to render twice during init and render', () => { const render = jest.fn(); const unmount = jest.fn(); @@ -151,7 +163,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const instantSearchInstance = createInstantSearch(); const { mainHelper: helper } = instantSearchInstance; - widget.init!(createInitOptions({ instantSearchInstance })); + widget.init(createInitOptions({ instantSearchInstance })); expect(render).toHaveBeenCalledTimes(1); expect(render).toHaveBeenLastCalledWith( @@ -176,7 +188,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect(lastRenderArgs(render).hasMapMoveSinceLastRefine()).toBe(false); expect(lastRenderArgs(render).isRefinedWithMap()).toBe(false); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper!.state, [ createSingleSearchResponse({ @@ -226,12 +238,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const helper = createFakeHelper(); - widget.init!(createInitOptions()); + widget.init(createInitOptions()); expect(render).toHaveBeenCalledTimes(1); expect(lastRenderArgs(render).isRefineOnMapMove()).toBe(false); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -254,9 +266,9 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const widget = customGeoSearch({}); const helper = createFakeHelper(); - widget.init!(createInitOptions()); + widget.init(createInitOptions()); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -299,9 +311,9 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }); const helper = createFakeHelper(); - widget.init!(createInitOptions()); + widget.init(createInitOptions()); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -338,7 +350,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // Simulate the configuration or external setter helper.setQueryParameter('aroundLatLng', '10, 12'); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -356,7 +368,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ true ); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -381,7 +393,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // Simulate the configuration or external setter helper.setQueryParameter('aroundLatLng', '12, 14'); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -416,8 +428,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ createSingleSearchResponse(), ]); - widget.init!(createInitOptions({ helper, state: helper.state })); - widget.render!( + widget.init(createInitOptions({ helper, state: helper.state })); + widget.render( createRenderOptions({ results, helper, @@ -461,14 +473,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ helper.setQueryParameter('aroundLatLng', '10,12'); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -477,7 +489,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -496,7 +508,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ helper.setQueryParameter('aroundLatLng', '14,16'); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -539,14 +551,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ helper.setQueryParameter('aroundLatLng', '10,12'); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -555,7 +567,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -574,7 +586,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ helper.setQueryParameter('aroundLatLng', '10,12'); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -618,14 +630,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // @ts-ignore helper.setQueryParameter('insideBoundingBox', '10,12,14,16'); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -634,7 +646,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -653,7 +665,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ helper.setQueryParameter('insideBoundingBox', undefined); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -697,14 +709,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // @ts-ignore helper.setQueryParameter('insideBoundingBox', '10,12,14,16'); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -713,7 +725,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -733,7 +745,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // @ts-ignore helper.setQueryParameter('insideBoundingBox', '12,14,16,18'); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -758,7 +770,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // @ts-ignore helper.setQueryParameter('insideBoundingBox', '10,12,12,14'); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -778,7 +790,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -805,7 +817,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // Simulate the configuration or external setter (like URLSync) helper.setQueryParameter('insideBoundingBox', undefined); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -830,7 +842,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ helper.setQueryParameter('insideBoundingBox', [[10, 12, 12, 14]]); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -850,7 +862,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -877,7 +889,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // Simulate the configuration or external setter (like URLSync) helper.setQueryParameter('insideBoundingBox', undefined); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -919,7 +931,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }), ]); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -934,7 +946,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -973,14 +985,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }), ]); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -995,7 +1007,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1036,7 +1048,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }), ]); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -1051,7 +1063,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1066,7 +1078,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).clearMapRefinement(); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1105,14 +1117,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }), ]); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1127,7 +1139,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).refine({ northEast, southWest }); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1142,7 +1154,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ lastRenderArgs(render).clearMapRefinement(); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1167,7 +1179,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const helper = createFakeHelper(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -1182,7 +1194,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect(render).toHaveBeenCalledTimes(1); expect(lastRenderArgs(render).isRefineOnMapMove()).toBe(false); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -1209,14 +1221,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const helper = createFakeHelper(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -1248,7 +1260,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const customGeoSearch = connectGeoSearch(render, unmount); const widget = customGeoSearch({}); - widget.init!(createInitOptions()); + widget.init(createInitOptions()); expect(render).toHaveBeenCalledTimes(1); expect(lastRenderArgs(render).hasMapMoveSinceLastRefine()).toBe(false); @@ -1258,7 +1270,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect(render).toHaveBeenCalledTimes(1); expect(lastRenderArgs(render).hasMapMoveSinceLastRefine()).toBe(true); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(createFakeHelper().state, [ createSingleSearchResponse({ @@ -1292,14 +1304,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ ), ]); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1335,14 +1347,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ ), ]); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results, helper, @@ -1381,7 +1393,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const expectation = new SearchParameters({ index: '' }); - const actual = widget.dispose!( + const actual = widget.dispose( createDisposeOptions({ state: helper.state }) ); @@ -1393,7 +1405,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ const render = () => {}; const customGeoSearch = connectGeoSearch(render); const widget = customGeoSearch({}); - expect(() => widget.dispose!(createDisposeOptions())).not.toThrow(); + expect(() => widget.dispose(createDisposeOptions())).not.toThrow(); }); }); diff --git a/packages/instantsearch.js/src/connectors/geo-search/connectGeoSearch.ts b/packages/instantsearch.js/src/connectors/geo-search/connectGeoSearch.ts index 5bec2acc5c..42c42b6b08 100644 --- a/packages/instantsearch.js/src/connectors/geo-search/connectGeoSearch.ts +++ b/packages/instantsearch.js/src/connectors/geo-search/connectGeoSearch.ts @@ -11,11 +11,15 @@ import type { SendEventForHits } from '../../lib/utils'; import type { BaseHit, Connector, + GeoHit, GeoLoc, - Hit, + IndexRenderState, InitOptions, + Renderer, RenderOptions, TransformItems, + UnknownWidgetParams, + Unmounter, WidgetRenderState, } from '../../types'; import type { @@ -23,6 +27,8 @@ import type { SearchParameters, } from 'algoliasearch-helper'; +export type { GeoHit } from '../../types'; + const withUsage = createDocumentationMessageGenerator({ name: 'geo-search', connector: true, @@ -41,9 +47,6 @@ function setBoundingBoxAsString(state: SearchParameters, value: string) { ); } -export type GeoHit> = Hit & - Required>; - type Bounds = { /** * The top right corner of the map view. @@ -55,7 +58,7 @@ type Bounds = { southWest: GeoLoc; }; -export type GeoSearchRenderState> = { +export type GeoSearchRenderState = BaseHit> = { /** * Reset the current bounding box refinement. */ @@ -104,9 +107,7 @@ export type GeoSearchRenderState> = { toggleRefineOnMapMove: () => void; }; -export type GeoSearchConnectorParams< - THit extends BaseHit = Record -> = { +export type GeoSearchConnectorParams = { /** * If true, refine will be triggered as you move the map. * @default true @@ -121,9 +122,7 @@ export type GeoSearchConnectorParams< const $$type = 'ais.geoSearch'; -export type GeoSearchWidgetDescription< - THit extends BaseHit = Record -> = { +export type GeoSearchWidgetDescription = { $$type: 'ais.geoSearch'; renderState: GeoSearchRenderState; indexRenderState: { @@ -146,8 +145,10 @@ export type GeoSearchWidgetDescription< }; }; -export type GeoSearchConnector> = - Connector, GeoSearchConnectorParams>; +export type GeoSearchConnector = Connector< + GeoSearchWidgetDescription, + GeoSearchConnectorParams +>; /** * The **GeoSearch** connector provides the logic to build a widget that will display the results on a map. It also provides a way to search for results based on their position. The connector provides functions to manage the search experience (search on map interaction or control the interaction for example). @@ -158,14 +159,24 @@ export type GeoSearchConnector> = * * Currently, the feature is not compatible with multiple values in the _geoloc attribute. */ -const connectGeoSearch: GeoSearchConnector = (renderFn, unmountFn = noop) => { +export default (function connectGeoSearch< + TWidgetParams extends UnknownWidgetParams +>( + renderFn: Renderer< + GeoSearchRenderState, + TWidgetParams & GeoSearchConnectorParams + >, + unmountFn: Unmounter = noop +) { checkRendering(renderFn, withUsage()); - return (widgetParams) => { + return ( + widgetParams: TWidgetParams & GeoSearchConnectorParams + ) => { const { enableRefineOnMapMove = true, transformItems = ((items) => items) as NonNullable< - GeoSearchConnectorParams['transformItems'] + GeoSearchConnectorParams['transformItems'] >, } = widgetParams || {}; @@ -362,7 +373,11 @@ const connectGeoSearch: GeoSearchConnector = (renderFn, unmountFn = noop) => { }; }, - getRenderState(renderState, renderOptions) { + getRenderState( + renderState, + renderOptions + // Type is explicitly redefined, to avoid having the TWidgetParams type in the definition + ): IndexRenderState & GeoSearchWidgetDescription['indexRenderState'] { return { ...renderState, geoSearch: this.getWidgetRenderState(renderOptions), @@ -409,6 +424,4 @@ const connectGeoSearch: GeoSearchConnector = (renderFn, unmountFn = noop) => { }, }; }; -}; - -export default connectGeoSearch; +} satisfies GeoSearchConnector); diff --git a/packages/instantsearch.js/src/connectors/hits/__tests__/connectHits-test.ts b/packages/instantsearch.js/src/connectors/hits/__tests__/connectHits-test.ts index ed7a36b492..b55e5b0c46 100644 --- a/packages/instantsearch.js/src/connectors/hits/__tests__/connectHits-test.ts +++ b/packages/instantsearch.js/src/connectors/hits/__tests__/connectHits-test.ts @@ -77,7 +77,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -90,7 +90,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co true ); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ hits: [] }), @@ -115,7 +115,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -138,7 +138,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const results = new SearchResults(helper.state, [ createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -163,7 +163,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -194,7 +194,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const results = new SearchResults(helper.state, [ createSingleSearchResponse(createSingleSearchResponse({ hits })), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -235,7 +235,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -256,7 +256,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const results = new SearchResults(helper.state, [ createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -291,8 +291,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co createSingleSearchResponse(), ]); - widget.init!(createInitOptions({ helper, state: helper.state })); - widget.render!( + widget.init(createInitOptions({ helper, state: helper.state })); + widget.render( createRenderOptions({ results, helper, @@ -314,7 +314,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -329,7 +329,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const results = new SearchResults(helper.state, [ createSingleSearchResponse({ hits, queryID: 'theQueryID' }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -378,7 +378,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -414,7 +414,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -465,7 +465,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ helper, state: helper.state, @@ -478,7 +478,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -619,7 +619,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const makeWidget = connectHits(render); const widget = makeWidget({}); - const actual = widget.getWidgetSearchParameters!(new SearchParameters(), { + const actual = widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, }); @@ -633,7 +633,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co escapeHTML: false, }); - const actual = widget.getWidgetSearchParameters!(new SearchParameters(), { + const actual = widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, }); @@ -652,7 +652,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co expect(unmountFn).toHaveBeenCalledTimes(0); - widget.dispose!(createDisposeOptions({ helper, state: helper.state })); + widget.dispose(createDisposeOptions({ helper, state: helper.state })); expect(unmountFn).toHaveBeenCalledTimes(1); }); @@ -665,7 +665,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const widget = makeWidget({}); expect(() => - widget.dispose!(createDisposeOptions({ helper, state: helper.state })) + widget.dispose(createDisposeOptions({ helper, state: helper.state })) ).not.toThrow(); }); @@ -686,12 +686,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co TAG_PLACEHOLDER.highlightPostTag ); - const nextState = widget.dispose!( + const nextState = widget.dispose( createDisposeOptions({ helper, state: helper.state, }) - ) as SearchParameters; + ); expect(nextState.highlightPreTag).toBeUndefined(); expect(nextState.highlightPostTag).toBeUndefined(); @@ -712,12 +712,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co expect(helper.state.highlightPreTag).toBe(''); expect(helper.state.highlightPostTag).toBe(''); - const nextState = widget.dispose!( + const nextState = widget.dispose( createDisposeOptions({ helper, state: helper.state, }) - ) as SearchParameters; + ); expect(nextState.highlightPreTag).toBe(''); expect(nextState.highlightPostTag).toBe(''); @@ -738,7 +738,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co state: helper.state, }); const instantSearchInstance = initOptions.instantSearchInstance; - widget.init!(initOptions); + widget.init(initOptions); const hits = [ { @@ -758,7 +758,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co const results = new SearchResults(helper.state, [ createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -780,7 +780,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co sendEventToInsights: jest.fn(), }); - widget.init!( + widget.init( createInitOptions({ instantSearchInstance, }) @@ -809,7 +809,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ instantSearchInstance, results, @@ -920,7 +920,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }); const instantSearchInstance = initOptions.instantSearchInstance; instantSearchInstance.sendEventToInsights = jest.fn(); - widget.init!(initOptions); + widget.init(initOptions); const hits = [ { @@ -935,7 +935,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, diff --git a/packages/instantsearch.js/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts b/packages/instantsearch.js/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts index 9635a9df0c..483a191bb3 100644 --- a/packages/instantsearch.js/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts +++ b/packages/instantsearch.js/src/connectors/hits/__tests__/connectHitsWithInsights-test.ts @@ -31,7 +31,7 @@ describe('connectHitsWithInsights', () => { const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ instantSearchInstance, state: helper.state, @@ -50,7 +50,7 @@ describe('connectHitsWithInsights', () => { createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ instantSearchInstance, state: helper.state, @@ -71,7 +71,7 @@ describe('connectHitsWithInsights', () => { const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -86,7 +86,7 @@ describe('connectHitsWithInsights', () => { createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -105,7 +105,7 @@ describe('connectHitsWithInsights', () => { const widget = makeWidget({}); const helper = algoliasearchHelper(createSearchClient(), '', {}); expect(() => { - widget.dispose!(createDisposeOptions({ helper, state: helper.state })); + widget.dispose(createDisposeOptions({ helper, state: helper.state })); }).not.toThrow(); }); }); diff --git a/packages/instantsearch.js/src/connectors/hits/connectHits.ts b/packages/instantsearch.js/src/connectors/hits/connectHits.ts index 8590a2538a..1ef040ccaa 100644 --- a/packages/instantsearch.js/src/connectors/hits/connectHits.ts +++ b/packages/instantsearch.js/src/connectors/hits/connectHits.ts @@ -17,6 +17,9 @@ import type { Hit, WidgetRenderState, BaseHit, + Unmounter, + Renderer, + IndexRenderState, } from '../../types'; import type { SearchResults } from 'algoliasearch-helper'; @@ -31,7 +34,7 @@ type Banner = NonNullable< >['widgets']['banners'] >[number]; -export type HitsRenderState = { +export type HitsRenderState = BaseHit> = { /** * The matched hits from Algolia API. */ @@ -58,7 +61,7 @@ export type HitsRenderState = { bindEvent: BindEventForHits; }; -export type HitsConnectorParams = { +export type HitsConnectorParams = BaseHit> = { /** * Whether to escape HTML tags from hits string values. * @@ -72,26 +75,27 @@ export type HitsConnectorParams = { transformItems?: TransformItems>; }; -export type HitsWidgetDescription = { - $$type: 'ais.hits'; - renderState: HitsRenderState; - indexRenderState: { - hits: WidgetRenderState, HitsConnectorParams>; +export type HitsWidgetDescription = BaseHit> = + { + $$type: 'ais.hits'; + renderState: HitsRenderState; + indexRenderState: { + hits: WidgetRenderState, HitsConnectorParams>; + }; }; -}; -export type HitsConnector = Connector< - HitsWidgetDescription, - HitsConnectorParams ->; +export type HitsConnector = BaseHit> = + Connector, HitsConnectorParams>; -const connectHits: HitsConnector = function connectHits( - renderFn, - unmountFn = noop +export default (function connectHits( + renderFn: Renderer, + unmountFn: Unmounter = noop ) { checkRendering(renderFn, withUsage()); - return (widgetParams) => { + return = BaseHit>( + widgetParams: TWidgetParams & HitsConnectorParams + ) => { const { // @MAJOR: this can default to false escapeHTML = true, @@ -129,7 +133,11 @@ const connectHits: HitsConnector = function connectHits( renderState.sendEvent('view:internal', renderState.hits); }, - getRenderState(renderState, renderOptions) { + getRenderState( + renderState, + renderOptions + // Type is explicitly redefined, to avoid having the TWidgetParams type in the definition + ): IndexRenderState & HitsWidgetDescription['indexRenderState'] { return { ...renderState, hits: this.getWidgetRenderState(renderOptions), @@ -214,7 +222,7 @@ const connectHits: HitsConnector = function connectHits( ); }, - getWidgetSearchParameters(state) { + getWidgetSearchParameters(state, _uiState) { if (!escapeHTML) { return state; } @@ -224,6 +232,4 @@ const connectHits: HitsConnector = function connectHits( }, }; }; -}; - -export default connectHits; +} satisfies HitsConnector); diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts b/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts index 997b921a5e..5868afe70b 100644 --- a/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts +++ b/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts @@ -28,7 +28,6 @@ import type { EscapedHits, SearchResponse, } from '../../../types'; -import type { SearchParameters } from 'algoliasearch-helper'; jest.mock('../../../lib/utils/hits-absolute-position', () => ({ // The real implementation creates a new array instance, which can cause bugs, @@ -69,6 +68,19 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi ); }); + it('accepts custom parameters', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customInfiniteHits = connectInfiniteHits<{ container: string }>( + render, + unmount + ); + const widget = customInfiniteHits({ container: '#container' }); + + expect(widget.$$type).toBe('ais.infiniteHits'); + }); + it('Renders during init and render', () => { const renderFn = jest.fn(); const instantSearchInstance = createInstantSearch(); @@ -82,7 +94,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ instantSearchInstance, state: helper.state, @@ -107,7 +119,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi true ); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ hits: [] }), @@ -144,7 +156,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -163,7 +175,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -187,7 +199,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits: otherHits }), ]); - widget.render!( + widget.render( createRenderOptions({ results: otherResults, state: helper.state, @@ -211,7 +223,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi helper.searchWithoutTriggeringOnStateChange = jest.fn(); helper.emit = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -230,7 +242,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -259,7 +271,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits: previousHits }), ]); - widget.render!( + widget.render( createRenderOptions({ results: previousResults, state: helper.state, @@ -282,7 +294,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi helper.overrideStateWithoutTriggeringChangeEvent = jest.fn(() => helper); helper.searchWithoutTriggeringOnStateChange = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -312,7 +324,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -331,7 +343,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -354,7 +366,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits: otherHits }), ]); - widget.render!( + widget.render( createRenderOptions({ results: otherResults, state: helper.state, @@ -376,14 +388,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results: new SearchResults(helper.state, [ @@ -398,7 +410,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi ); renderFn.mock.calls[1][0].showMore(); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results: new SearchResults(helper.state, [ @@ -414,7 +426,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi expect(helper.state.page).toEqual(1); renderFn.mock.calls[2][0].showMore(); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results: new SearchResults(helper.state, [ @@ -430,7 +442,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi expect(helper.state.page).toEqual(2); helper.setPage(0); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results: new SearchResults(helper.state, [ @@ -461,7 +473,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -490,7 +502,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -529,7 +541,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -555,7 +567,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -591,8 +603,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse(), ]); - widget.init!(createInitOptions({ helper, state: helper.state })); - widget.render!( + widget.init(createInitOptions({ helper, state: helper.state })); + widget.render( createRenderOptions({ results, helper, @@ -625,7 +637,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -661,7 +673,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -710,7 +722,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -732,7 +744,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits, queryID: 'theQueryID' }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -768,14 +780,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const helper = algoliasearchHelper({} as SearchClient, '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, }) ); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -791,7 +803,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi helper.setPage(1); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -813,7 +825,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi false ); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(helper.state, [ createSingleSearchResponse({ @@ -846,7 +858,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createInitOptions(); - widget.init!( + widget.init( createInitOptions({ state: helper.state, helper, @@ -859,7 +871,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }), ]); - widget.render!( + widget.render( createRenderOptions({ state: helper.state, results, @@ -893,7 +905,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi instantSearchInstance, }); - widget.init!(initOptions); + widget.init(initOptions); const renderWidget = ( args: Partial> @@ -905,7 +917,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi ...args, }); - widget.render!(renderOptions); + widget.render(renderOptions); }; return { helper, renderFn, renderWidget }; } @@ -1008,7 +1020,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi instantSearchInstance, }); - widget.init!(initOptions); + widget.init(initOptions); const renderWidget = ( args: Partial> @@ -1020,7 +1032,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi ...args, }); - widget.render!(renderOptions); + widget.render(renderOptions); }; return { helper, renderFn, renderWidget }; } @@ -1114,7 +1126,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi expect(unmountFn).toHaveBeenCalledTimes(0); - widget.dispose!(createDisposeOptions({ helper, state: helper.state })); + widget.dispose(createDisposeOptions({ helper, state: helper.state })); expect(unmountFn).toHaveBeenCalledTimes(1); }); @@ -1127,7 +1139,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const widget = makeWidget({}); expect(() => - widget.dispose!(createDisposeOptions({ helper, state: helper.state })) + widget.dispose(createDisposeOptions({ helper, state: helper.state })) ).not.toThrow(); }); @@ -1148,12 +1160,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi TAG_PLACEHOLDER.highlightPostTag ); - const nextState = widget.dispose!( + const nextState = widget.dispose( createDisposeOptions({ helper, state: helper.state, }) - ) as SearchParameters; + ); expect(nextState.highlightPreTag).toBeUndefined(); expect(nextState.highlightPostTag).toBeUndefined(); @@ -1174,12 +1186,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi expect(helper.state.highlightPreTag).toBe(''); expect(helper.state.highlightPostTag).toBe(''); - const nextState = widget.dispose!( + const nextState = widget.dispose( createDisposeOptions({ helper, state: helper.state, }) - ) as SearchParameters; + ); expect(nextState.highlightPreTag).toBe(''); expect(nextState.highlightPostTag).toBe(''); @@ -1196,12 +1208,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi expect(helper.state.page).toBe(5); - const nextState = widget.dispose!( + const nextState = widget.dispose( createDisposeOptions({ helper, state: helper.state, }) - ) as SearchParameters; + ); expect(nextState.page).toBeUndefined(); }); @@ -1591,7 +1603,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi state: helper.state, }); const instantSearchInstance = initOptions.instantSearchInstance; - widget.init!(initOptions); + widget.init(initOptions); const hits = [ { @@ -1611,7 +1623,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi const results = new SearchResults(helper.state, [ createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, @@ -1730,7 +1742,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }); const instantSearchInstance = initOptions.instantSearchInstance; instantSearchInstance.sendEventToInsights = jest.fn(); - widget.init!(initOptions); + widget.init(initOptions); const hits = [ { @@ -1745,7 +1757,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptions({ results, state: helper.state, diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts b/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts index 91a404e063..9940e03eca 100644 --- a/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts +++ b/packages/instantsearch.js/src/connectors/infinite-hits/__tests__/connectInfiniteHitsWithInsights-test.ts @@ -53,7 +53,7 @@ describe('connectInfiniteHitsWithInsights', () => { const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptionsWithInsights({ state: helper.state, helper, @@ -71,7 +71,7 @@ describe('connectInfiniteHitsWithInsights', () => { createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptionsWithInsights({ state: helper.state, results, @@ -91,7 +91,7 @@ describe('connectInfiniteHitsWithInsights', () => { const helper = algoliasearchHelper(createSearchClient(), '', {}); helper.search = jest.fn(); - widget.init!( + widget.init( createInitOptionsWithInsights({ state: helper.state, helper, @@ -106,7 +106,7 @@ describe('connectInfiniteHitsWithInsights', () => { createSingleSearchResponse({ hits }), ]); - widget.render!( + widget.render( createRenderOptionsWithInsights({ state: helper.state, results, @@ -125,7 +125,7 @@ describe('connectInfiniteHitsWithInsights', () => { const helper = algoliasearchHelper(createSearchClient(), '', {}); const widget = makeWidget({}); expect(() => - widget.dispose!(createDisposeOptions({ helper, state: helper.state })) + widget.dispose(createDisposeOptions({ helper, state: helper.state })) ).not.toThrow(); }); }); diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts b/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts index ca4135c7a6..6221ce7c37 100644 --- a/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts +++ b/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts @@ -19,6 +19,10 @@ import type { Hit, WidgetRenderState, BaseHit, + Renderer, + Unmounter, + UnknownWidgetParams, + IndexRenderState, } from '../../types'; import type { AlgoliaSearchHelper as Helper, @@ -27,17 +31,17 @@ import type { SearchResults, } from 'algoliasearch-helper'; -export type InfiniteHitsCachedHits = { +export type InfiniteHitsCachedHits> = { [page: number]: Array>; }; -type Read = ({ +type Read> = ({ state, }: { state: PlainSearchParameters; }) => InfiniteHitsCachedHits | null; -type Write = ({ +type Write> = ({ state, hits, }: { @@ -45,12 +49,14 @@ type Write = ({ hits: InfiniteHitsCachedHits; }) => void; -export type InfiniteHitsCache = { +export type InfiniteHitsCache = BaseHit> = { read: Read; write: Write; }; -export type InfiniteHitsConnectorParams = { +export type InfiniteHitsConnectorParams< + THit extends NonNullable = BaseHit +> = { /** * Escapes HTML entities from hits string values. * @@ -79,7 +85,9 @@ export type InfiniteHitsConnectorParams = { cache?: InfiniteHitsCache; }; -export type InfiniteHitsRenderState = { +export type InfiniteHitsRenderState< + THit extends NonNullable = BaseHit +> = { /** * Loads the previous results. */ @@ -131,7 +139,9 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); -export type InfiniteHitsWidgetDescription = { +export type InfiniteHitsWidgetDescription< + THit extends NonNullable = BaseHit +> = { $$type: 'ais.infiniteHits'; renderState: InfiniteHitsRenderState; indexRenderState: { @@ -145,10 +155,11 @@ export type InfiniteHitsWidgetDescription = { }; }; -export type InfiniteHitsConnector = Connector< - InfiniteHitsWidgetDescription, - InfiniteHitsConnectorParams ->; +export type InfiniteHitsConnector = BaseHit> = + Connector< + InfiniteHitsWidgetDescription, + InfiniteHitsConnectorParams + >; function getStateWithoutPage(state: PlainSearchParameters) { const { page, ...rest } = state || {}; @@ -160,7 +171,9 @@ function normalizeState(state: PlainSearchParameters) { return rest; } -function getInMemoryCache(): InfiniteHitsCache { +function getInMemoryCache< + THit extends NonNullable +>(): InfiniteHitsCache { let cachedHits: InfiniteHitsCachedHits | null = null; let cachedState: PlainSearchParameters | null = null; return { @@ -176,7 +189,7 @@ function getInMemoryCache(): InfiniteHitsCache { }; } -function extractHitsFromCachedHits( +function extractHitsFromCachedHits>( cachedHits: InfiniteHitsCachedHits ) { return Object.keys(cachedHits) @@ -187,23 +200,24 @@ function extractHitsFromCachedHits( }, []); } -const connectInfiniteHits: InfiniteHitsConnector = function connectInfiniteHits( - renderFn, - unmountFn = noop +export default (function connectInfiniteHits< + TWidgetParams extends UnknownWidgetParams +>( + renderFn: Renderer, + unmountFn: Unmounter = noop ) { checkRendering(renderFn, withUsage()); - // @TODO: this should be a generic, but a Connector can not yet be generic itself - type THit = BaseHit; - - return (widgetParams) => { + return = BaseHit>( + widgetParams: TWidgetParams & InfiniteHitsConnectorParams + ) => { const { // @MAJOR: this can default to false escapeHTML = true, transformItems = ((items) => items) as NonNullable< - InfiniteHitsConnectorParams['transformItems'] + InfiniteHitsConnectorParams['transformItems'] >, - cache = getInMemoryCache(), + cache = getInMemoryCache(), } = widgetParams || {}; let showPrevious: () => void; let showMore: () => void; @@ -293,7 +307,11 @@ const connectInfiniteHits: InfiniteHitsConnector = function connectInfiniteHits( sendEvent('view:internal', widgetRenderState.currentPageHits); }, - getRenderState(renderState, renderOptions) { + getRenderState( + renderState, + renderOptions + // Type is explicitly redefined, to avoid having the TWidgetParams type in the definition + ): IndexRenderState & InfiniteHitsWidgetDescription['indexRenderState'] { return { ...renderState, infiniteHits: this.getWidgetRenderState(renderOptions), @@ -464,6 +482,4 @@ const connectInfiniteHits: InfiniteHitsConnector = function connectInfiniteHits( }, }; }; -}; - -export default connectInfiniteHits; +} satisfies InfiniteHitsConnector); diff --git a/packages/instantsearch.js/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts b/packages/instantsearch.js/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts new file mode 100644 index 0000000000..2898a6e451 --- /dev/null +++ b/packages/instantsearch.js/src/connectors/looking-similar/__tests__/connectLookingSimilar-test.ts @@ -0,0 +1,135 @@ +/** + * @jest-environment jsdom + */ + +import { createSearchClient } from '@instantsearch/mocks'; +import algoliasearchHelper, { RecommendParameters } from 'algoliasearch-helper'; + +import { + createInitOptions, + createRenderOptions, +} from '../../../../test/createWidget'; +import connectLookingSimilar from '../connectLookingSimilar'; + +describe('connectLookingSimilar', () => { + it('throws without render function', () => { + expect(() => { + // @ts-expect-error + connectLookingSimilar()({}); + }).toThrowErrorMatchingInlineSnapshot(` + "The render function is not valid (received type Undefined). + + See documentation: https://www.algolia.com/doc/api-reference/widgets/looking-similar/js/#connector" + `); + }); + + it('is a widget', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customLookingSimilar = connectLookingSimilar(render, unmount); + const widget = customLookingSimilar({ objectIDs: ['1'] }); + + expect(widget).toEqual( + expect.objectContaining({ + $$type: 'ais.lookingSimilar', + init: expect.any(Function), + render: expect.any(Function), + dispose: expect.any(Function), + }) + ); + }); + + it('accepts custom parameters', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customLookingSimilar = connectLookingSimilar<{ + container: string; + }>(render, unmount); + const widget = customLookingSimilar({ + container: '#container', + objectIDs: ['1'], + }); + + expect(widget.$$type).toBe('ais.lookingSimilar'); + }); + + it('Renders during init and render', () => { + const renderFn = jest.fn(); + const makeWidget = connectLookingSimilar(renderFn); + const widget = makeWidget({ objectIDs: ['1'] }); + + // test if widget is not rendered yet at this point + expect(renderFn).toHaveBeenCalledTimes(0); + + const helper = algoliasearchHelper(createSearchClient(), '', {}); + helper.search = jest.fn(); + + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); + + expect(renderFn).toHaveBeenCalledTimes(1); + expect(renderFn).toHaveBeenLastCalledWith( + expect.objectContaining({ widgetParams: { objectIDs: ['1'] } }), + true + ); + + const renderOptions = createRenderOptions({ + helper, + }); + + widget.render(renderOptions); + + expect(renderFn).toHaveBeenCalledTimes(2); + expect(renderFn).toHaveBeenLastCalledWith( + expect.objectContaining({ widgetParams: { objectIDs: ['1'] } }), + false + ); + }); + + describe('getWidgetParameters', () => { + it('forwards widgetParams to the recommend state', () => { + const render = () => {}; + const makeWidget = connectLookingSimilar(render); + const widget = makeWidget({ + objectIDs: ['1', '2'], + limit: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + escapeHTML: false, + }); + + // @ts-expect-error + const actual = widget.getWidgetParameters(new RecommendParameters(), { + uiState: {}, + }); + + expect(actual).toEqual( + new RecommendParameters() + .addLookingSimilar({ + // @ts-expect-error + $$id: widget.$$id, + objectID: '1', + maxRecommendations: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + fallbackParameters: {}, + }) + .addLookingSimilar({ + // @ts-expect-error + $$id: widget.$$id, + objectID: '2', + maxRecommendations: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + fallbackParameters: {}, + }) + ); + }); + }); +}); diff --git a/packages/instantsearch.js/src/connectors/looking-similar/connectLookingSimilar.ts b/packages/instantsearch.js/src/connectors/looking-similar/connectLookingSimilar.ts index 772cbe424c..34c86903c6 100644 --- a/packages/instantsearch.js/src/connectors/looking-similar/connectLookingSimilar.ts +++ b/packages/instantsearch.js/src/connectors/looking-similar/connectLookingSimilar.ts @@ -6,7 +6,15 @@ import { TAG_PLACEHOLDER, } from '../../lib/utils'; -import type { Connector, TransformItems, Hit, BaseHit } from '../../types'; +import type { + Connector, + TransformItems, + Hit, + BaseHit, + Renderer, + Unmounter, + UnknownWidgetParams, +} from '../../types'; import type { PlainSearchParameters, RecommendResultItem, @@ -17,14 +25,18 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); -export type LookingSimilarRenderState = { +export type LookingSimilarRenderState< + THit extends NonNullable = BaseHit +> = { /** * The matched recommendations from the Algolia API. */ items: Array>; }; -export type LookingSimilarConnectorParams = { +export type LookingSimilarConnectorParams< + THit extends NonNullable = BaseHit +> = { /** * The `objectIDs` of the items to get similar looking products from. */ @@ -63,112 +75,123 @@ export type LookingSimilarConnectorParams = { transformItems?: TransformItems, { results: RecommendResultItem }>; }; -export type LookingSimilarWidgetDescription = { +export type LookingSimilarWidgetDescription< + THit extends NonNullable = BaseHit +> = { $$type: 'ais.lookingSimilar'; renderState: LookingSimilarRenderState; }; -export type LookingSimilarConnector = Connector< +export type LookingSimilarConnector< + THit extends NonNullable = BaseHit +> = Connector< LookingSimilarWidgetDescription, LookingSimilarConnectorParams >; -const connectLookingSimilar: LookingSimilarConnector = - function connectLookingSimilar(renderFn, unmountFn = noop) { - checkRendering(renderFn, withUsage()); - - return function LookingSimilar(widgetParams) { - const { - // @MAJOR: this can default to false - escapeHTML = true, - objectIDs, - limit, - threshold, - fallbackParameters, - queryParameters, - transformItems = ((items) => items) as NonNullable< - LookingSimilarConnectorParams['transformItems'] - >, - } = widgetParams || {}; - - if (!objectIDs || objectIDs.length === 0) { - throw new Error(withUsage('The `objectIDs` option is required.')); - } - - return { - dependsOn: 'recommend', - $$type: 'ais.lookingSimilar', - - init(initOptions) { - renderFn( - { - ...this.getWidgetRenderState(initOptions), - instantSearchInstance: initOptions.instantSearchInstance, - }, - true - ); - }, - - render(renderOptions) { - const renderState = this.getWidgetRenderState(renderOptions); - - renderFn( - { - ...renderState, - instantSearchInstance: renderOptions.instantSearchInstance, - }, - false - ); - }, - - getRenderState(renderState) { - return renderState; - }, - - getWidgetRenderState({ results }) { - if (results === null || results === undefined) { - return { items: [], widgetParams }; - } - - if (escapeHTML && results.hits.length > 0) { - results.hits = escapeHits(results.hits); - } - - return { - items: transformItems(results.hits, { - results: results as RecommendResultItem, +export default (function connectLookingSimilar< + TWidgetParams extends UnknownWidgetParams +>( + renderFn: Renderer< + LookingSimilarRenderState, + TWidgetParams & LookingSimilarConnectorParams + >, + unmountFn: Unmounter = noop +) { + checkRendering(renderFn, withUsage()); + + return = BaseHit>( + widgetParams: TWidgetParams & LookingSimilarConnectorParams + ) => { + const { + // @MAJOR: this can default to false + escapeHTML = true, + objectIDs, + limit, + threshold, + fallbackParameters, + queryParameters, + transformItems = ((items) => items) as NonNullable< + LookingSimilarConnectorParams['transformItems'] + >, + } = widgetParams || {}; + + if (!objectIDs || objectIDs.length === 0) { + throw new Error(withUsage('The `objectIDs` option is required.')); + } + + return { + dependsOn: 'recommend', + $$type: 'ais.lookingSimilar', + + init(initOptions) { + renderFn( + { + ...this.getWidgetRenderState(initOptions), + instantSearchInstance: initOptions.instantSearchInstance, + }, + true + ); + }, + + render(renderOptions) { + const renderState = this.getWidgetRenderState(renderOptions); + + renderFn( + { + ...renderState, + instantSearchInstance: renderOptions.instantSearchInstance, + }, + false + ); + }, + + getRenderState(renderState) { + return renderState; + }, + + getWidgetRenderState({ results }) { + if (results === null || results === undefined) { + return { items: [], widgetParams }; + } + + if (escapeHTML && results.hits.length > 0) { + results.hits = escapeHits(results.hits); + } + + return { + items: transformItems(results.hits, { + results: results as RecommendResultItem, + }), + widgetParams, + }; + }, + + dispose({ recommendState }) { + unmountFn(); + return recommendState.removeParams(this.$$id!); + }, + + getWidgetParameters(state) { + return objectIDs.reduce( + (acc, objectID) => + acc.addLookingSimilar({ + objectID, + maxRecommendations: limit, + threshold, + fallbackParameters: { + ...fallbackParameters, + ...(escapeHTML ? TAG_PLACEHOLDER : {}), + }, + queryParameters: { + ...queryParameters, + ...(escapeHTML ? TAG_PLACEHOLDER : {}), + }, + $$id: this.$$id!, }), - widgetParams, - }; - }, - - dispose({ recommendState }) { - unmountFn(); - return recommendState.removeParams(this.$$id!); - }, - - getWidgetParameters(state) { - return objectIDs.reduce( - (acc, objectID) => - acc.addLookingSimilar({ - objectID, - maxRecommendations: limit, - threshold, - fallbackParameters: { - ...fallbackParameters, - ...(escapeHTML ? TAG_PLACEHOLDER : {}), - }, - queryParameters: { - ...queryParameters, - ...(escapeHTML ? TAG_PLACEHOLDER : {}), - }, - $$id: this.$$id!, - }), - state.removeParams(this.$$id!) - ); - }, - }; + state.removeParams(this.$$id!) + ); + }, }; }; - -export default connectLookingSimilar; +} satisfies LookingSimilarConnector); diff --git a/packages/instantsearch.js/src/connectors/query-rules/__tests__/connectQueryRules-test.ts b/packages/instantsearch.js/src/connectors/query-rules/__tests__/connectQueryRules-test.ts index eef4ff15f9..d88ff0e893 100644 --- a/packages/instantsearch.js/src/connectors/query-rules/__tests__/connectQueryRules-test.ts +++ b/packages/instantsearch.js/src/connectors/query-rules/__tests__/connectQueryRules-test.ts @@ -354,7 +354,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/query-rules hierarchicalMenu: {}, queryRules: { items: ['lions', 'eggs'], - widgetParams: { transformItems: (items) => items }, + widgetParams: {}, }, }, createRenderOptions({ diff --git a/packages/instantsearch.js/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts b/packages/instantsearch.js/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts new file mode 100644 index 0000000000..bea1e92fe5 --- /dev/null +++ b/packages/instantsearch.js/src/connectors/related-products/__tests__/connectRelatedProducts-test.ts @@ -0,0 +1,135 @@ +/** + * @jest-environment jsdom + */ + +import { createSearchClient } from '@instantsearch/mocks'; +import algoliasearchHelper, { RecommendParameters } from 'algoliasearch-helper'; + +import { + createInitOptions, + createRenderOptions, +} from '../../../../test/createWidget'; +import connectRelatedProducts from '../connectRelatedProducts'; + +describe('connectRelatedProducts', () => { + it('throws without render function', () => { + expect(() => { + // @ts-expect-error + connectRelatedProducts()({}); + }).toThrowErrorMatchingInlineSnapshot(` + "The render function is not valid (received type Undefined). + + See documentation: https://www.algolia.com/doc/api-reference/widgets/related-products/js/#connector" + `); + }); + + it('is a widget', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customRelatedProducts = connectRelatedProducts(render, unmount); + const widget = customRelatedProducts({ objectIDs: ['1'] }); + + expect(widget).toEqual( + expect.objectContaining({ + $$type: 'ais.relatedProducts', + init: expect.any(Function), + render: expect.any(Function), + dispose: expect.any(Function), + }) + ); + }); + + it('accepts custom parameters', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customRelatedProducts = connectRelatedProducts<{ + container: string; + }>(render, unmount); + const widget = customRelatedProducts({ + container: '#container', + objectIDs: ['1'], + }); + + expect(widget.$$type).toBe('ais.relatedProducts'); + }); + + it('Renders during init and render', () => { + const renderFn = jest.fn(); + const makeWidget = connectRelatedProducts(renderFn); + const widget = makeWidget({ objectIDs: ['1'] }); + + // test if widget is not rendered yet at this point + expect(renderFn).toHaveBeenCalledTimes(0); + + const helper = algoliasearchHelper(createSearchClient(), '', {}); + helper.search = jest.fn(); + + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); + + expect(renderFn).toHaveBeenCalledTimes(1); + expect(renderFn).toHaveBeenLastCalledWith( + expect.objectContaining({ widgetParams: { objectIDs: ['1'] } }), + true + ); + + const renderOptions = createRenderOptions({ + helper, + }); + + widget.render(renderOptions); + + expect(renderFn).toHaveBeenCalledTimes(2); + expect(renderFn).toHaveBeenLastCalledWith( + expect.objectContaining({ widgetParams: { objectIDs: ['1'] } }), + false + ); + }); + + describe('getWidgetParameters', () => { + it('forwards widgetParams to the recommend state', () => { + const render = () => {}; + const makeWidget = connectRelatedProducts(render); + const widget = makeWidget({ + objectIDs: ['1', '2'], + limit: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + escapeHTML: false, + }); + + // @ts-expect-error + const actual = widget.getWidgetParameters(new RecommendParameters(), { + uiState: {}, + }); + + expect(actual).toEqual( + new RecommendParameters() + .addRelatedProducts({ + // @ts-expect-error + $$id: widget.$$id, + objectID: '1', + maxRecommendations: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + fallbackParameters: {}, + }) + .addRelatedProducts({ + // @ts-expect-error + $$id: widget.$$id, + objectID: '2', + maxRecommendations: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + fallbackParameters: {}, + }) + ); + }); + }); +}); diff --git a/packages/instantsearch.js/src/connectors/related-products/connectRelatedProducts.ts b/packages/instantsearch.js/src/connectors/related-products/connectRelatedProducts.ts index a57ed52c77..db6c8678c6 100644 --- a/packages/instantsearch.js/src/connectors/related-products/connectRelatedProducts.ts +++ b/packages/instantsearch.js/src/connectors/related-products/connectRelatedProducts.ts @@ -6,7 +6,15 @@ import { TAG_PLACEHOLDER, } from '../../lib/utils'; -import type { Connector, TransformItems, Hit, BaseHit } from '../../types'; +import type { + Connector, + TransformItems, + Hit, + BaseHit, + Renderer, + Unmounter, + UnknownWidgetParams, +} from '../../types'; import type { PlainSearchParameters, RecommendResultItem, @@ -17,14 +25,18 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); -export type RelatedProductsRenderState = { +export type RelatedProductsRenderState< + THit extends NonNullable = BaseHit +> = { /** * The matched recommendations from the Algolia API. */ items: Array>; }; -export type RelatedProductsConnectorParams = { +export type RelatedProductsConnectorParams< + THit extends NonNullable = BaseHit +> = { /** * The `objectIDs` of the items to get related products from. */ @@ -63,113 +75,123 @@ export type RelatedProductsConnectorParams = { transformItems?: TransformItems, { results: RecommendResultItem }>; }; -export type RelatedProductsWidgetDescription = { +export type RelatedProductsWidgetDescription< + THit extends NonNullable = BaseHit +> = { $$type: 'ais.relatedProducts'; renderState: RelatedProductsRenderState; }; -export type RelatedProductsConnector = - Connector< - RelatedProductsWidgetDescription, - RelatedProductsConnectorParams - >; - -const connectRelatedProducts: RelatedProductsConnector = - function connectRelatedProducts(renderFn, unmountFn = noop) { - checkRendering(renderFn, withUsage()); - - return function relatedProducts(widgetParams) { - const { - // @MAJOR: this can default to false - escapeHTML = true, - objectIDs, - limit, - threshold, - fallbackParameters, - queryParameters, - transformItems = ((items) => items) as NonNullable< - RelatedProductsConnectorParams['transformItems'] - >, - } = widgetParams || {}; - - if (!objectIDs || objectIDs.length === 0) { - throw new Error(withUsage('The `objectIDs` option is required.')); - } - - return { - dependsOn: 'recommend', - $$type: 'ais.relatedProducts', - - init(initOptions) { - renderFn( - { - ...this.getWidgetRenderState(initOptions), - instantSearchInstance: initOptions.instantSearchInstance, - }, - true - ); - }, - - render(renderOptions) { - const renderState = this.getWidgetRenderState(renderOptions); - - renderFn( - { - ...renderState, - instantSearchInstance: renderOptions.instantSearchInstance, - }, - false - ); - }, - - getRenderState(renderState) { - return renderState; - }, - - getWidgetRenderState({ results }) { - if (results === null || results === undefined) { - return { items: [], widgetParams }; - } - - if (escapeHTML && results.hits.length > 0) { - results.hits = escapeHits(results.hits); - } - - return { - items: transformItems(results.hits, { - results: results as RecommendResultItem, +export type RelatedProductsConnector< + THit extends NonNullable = BaseHit +> = Connector< + RelatedProductsWidgetDescription, + RelatedProductsConnectorParams +>; + +export default (function connectRelatedProducts< + TWidgetParams extends UnknownWidgetParams +>( + renderFn: Renderer< + RelatedProductsRenderState, + RelatedProductsConnectorParams & TWidgetParams + >, + unmountFn: Unmounter = noop +) { + checkRendering(renderFn, withUsage()); + + return = BaseHit>( + widgetParams: TWidgetParams & RelatedProductsConnectorParams + ) => { + const { + // @MAJOR: this can default to false + escapeHTML = true, + objectIDs, + limit, + threshold, + fallbackParameters, + queryParameters, + transformItems = ((items) => items) as NonNullable< + RelatedProductsConnectorParams['transformItems'] + >, + } = widgetParams || {}; + + if (!objectIDs || objectIDs.length === 0) { + throw new Error(withUsage('The `objectIDs` option is required.')); + } + + return { + dependsOn: 'recommend', + $$type: 'ais.relatedProducts', + + init(initOptions) { + renderFn( + { + ...this.getWidgetRenderState(initOptions), + instantSearchInstance: initOptions.instantSearchInstance, + }, + true + ); + }, + + render(renderOptions) { + const renderState = this.getWidgetRenderState(renderOptions); + + renderFn( + { + ...renderState, + instantSearchInstance: renderOptions.instantSearchInstance, + }, + false + ); + }, + + getRenderState(renderState) { + return renderState; + }, + + getWidgetRenderState({ results }) { + if (results === null || results === undefined) { + return { items: [], widgetParams }; + } + + if (escapeHTML && results.hits.length > 0) { + results.hits = escapeHits(results.hits); + } + + return { + items: transformItems(results.hits, { + results: results as RecommendResultItem, + }), + widgetParams, + }; + }, + + dispose({ recommendState }) { + unmountFn(); + return recommendState.removeParams(this.$$id!); + }, + + getWidgetParameters(state) { + return objectIDs.reduce( + (acc, objectID) => + acc.addRelatedProducts({ + objectID, + maxRecommendations: limit, + threshold, + fallbackParameters: { + ...fallbackParameters, + ...(escapeHTML ? TAG_PLACEHOLDER : {}), + }, + queryParameters: { + ...queryParameters, + ...(escapeHTML ? TAG_PLACEHOLDER : {}), + }, + $$id: this.$$id!, }), - widgetParams, - }; - }, - - dispose({ recommendState }) { - unmountFn(); - return recommendState.removeParams(this.$$id!); - }, - - getWidgetParameters(state) { - return objectIDs.reduce( - (acc, objectID) => - acc.addRelatedProducts({ - objectID, - maxRecommendations: limit, - threshold, - fallbackParameters: { - ...fallbackParameters, - ...(escapeHTML ? TAG_PLACEHOLDER : {}), - }, - queryParameters: { - ...queryParameters, - ...(escapeHTML ? TAG_PLACEHOLDER : {}), - }, - $$id: this.$$id!, - }), - state.removeParams(this.$$id!) - ); - }, - }; + state.removeParams(this.$$id!) + ); + }, }; }; - -export default connectRelatedProducts; +} satisfies RelatedProductsConnector); diff --git a/packages/instantsearch.js/src/connectors/sort-by/__tests__/connectSortBy-test.ts b/packages/instantsearch.js/src/connectors/sort-by/__tests__/connectSortBy-test.ts index 15bb73c473..58d69c6665 100644 --- a/packages/instantsearch.js/src/connectors/sort-by/__tests__/connectSortBy-test.ts +++ b/packages/instantsearch.js/src/connectors/sort-by/__tests__/connectSortBy-test.ts @@ -13,7 +13,7 @@ import { createInitOptions, createRenderOptions, } from '../../../../test/createWidget'; -import index from '../../../widgets/index/index'; +import { index } from '../../../widgets'; import connectSortBy from '../connectSortBy'; import type { SortByRenderState } from '../connectSortBy'; diff --git a/packages/instantsearch.js/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts b/packages/instantsearch.js/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts new file mode 100644 index 0000000000..1d955bbf3c --- /dev/null +++ b/packages/instantsearch.js/src/connectors/trending-items/__tests__/connectTrendingItems-test.ts @@ -0,0 +1,173 @@ +/** + * @jest-environment jsdom + */ + +import { createSearchClient } from '@instantsearch/mocks'; +import algoliasearchHelper, { RecommendParameters } from 'algoliasearch-helper'; + +import { + createInitOptions, + createRenderOptions, +} from '../../../../test/createWidget'; +import connectTrendingItems from '../connectTrendingItems'; + +describe('connectTrendingItems', () => { + it('throws without render function', () => { + expect(() => { + // @ts-expect-error + connectTrendingItems()({}); + }).toThrowErrorMatchingInlineSnapshot(` + "The render function is not valid (received type Undefined). + + See documentation: https://www.algolia.com/doc/api-reference/widgets/trending-items/js/#connector" + `); + }); + + it('throws if facetName is provided without facetValue', () => { + expect(() => { + connectTrendingItems(() => {})({ facetName: 'key' }); + }).toThrowErrorMatchingInlineSnapshot(` + "When you provide facetName (received type String), you must also provide facetValue (received type Undefined). + + See documentation: https://www.algolia.com/doc/api-reference/widgets/trending-items/js/#connector" + `); + }); + + it('throws if facetValue is provided without facetName', () => { + expect(() => { + connectTrendingItems(() => {})({ facetValue: 'value' }); + }).toThrowErrorMatchingInlineSnapshot(` + "When you provide facetName (received type Undefined), you must also provide facetValue (received type String). + + See documentation: https://www.algolia.com/doc/api-reference/widgets/trending-items/js/#connector" + `); + }); + + it('is a widget', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customTrendingItems = connectTrendingItems(render, unmount); + const widget = customTrendingItems({}); + + expect(widget).toEqual( + expect.objectContaining({ + $$type: 'ais.trendingItems', + init: expect.any(Function), + render: expect.any(Function), + dispose: expect.any(Function), + }) + ); + }); + + it('accepts custom parameters', () => { + const render = jest.fn(); + const unmount = jest.fn(); + + const customTrendingItems = connectTrendingItems<{ + container: string; + }>(render, unmount); + const widget = customTrendingItems({ container: '#container' }); + + expect(widget.$$type).toBe('ais.trendingItems'); + }); + + it('Renders during init and render', () => { + const renderFn = jest.fn(); + const makeWidget = connectTrendingItems(renderFn); + const widget = makeWidget({}); + + // test if widget is not rendered yet at this point + expect(renderFn).toHaveBeenCalledTimes(0); + + const helper = algoliasearchHelper(createSearchClient(), '', {}); + helper.search = jest.fn(); + + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); + + expect(renderFn).toHaveBeenCalledTimes(1); + expect(renderFn).toHaveBeenLastCalledWith( + expect.objectContaining({ widgetParams: {} }), + true + ); + + const renderOptions = createRenderOptions({ + helper, + }); + + widget.render(renderOptions); + + expect(renderFn).toHaveBeenCalledTimes(2); + expect(renderFn).toHaveBeenLastCalledWith( + expect.objectContaining({ widgetParams: {} }), + false + ); + }); + + describe('getWidgetParameters', () => { + it('forwards widgetParams to the recommend state', () => { + const render = () => {}; + const makeWidget = connectTrendingItems(render); + const widget = makeWidget({ + limit: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + escapeHTML: false, + }); + + // @ts-expect-error + const actual = widget.getWidgetParameters(new RecommendParameters(), { + uiState: {}, + }); + + expect(actual).toEqual( + new RecommendParameters().addTrendingItems({ + // @ts-expect-error + $$id: widget.$$id, + facetName: undefined, + facetValue: undefined, + maxRecommendations: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + fallbackParameters: {}, + }) + ); + }); + + it('forwards widgetParams to the recommend state with facet', () => { + const render = () => {}; + const makeWidget = connectTrendingItems(render); + const widget = makeWidget({ + facetName: 'key', + facetValue: 'value', + limit: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + escapeHTML: false, + }); + + // @ts-expect-error + const actual = widget.getWidgetParameters(new RecommendParameters(), { + uiState: {}, + }); + + expect(actual).toEqual( + new RecommendParameters().addTrendingItems({ + // @ts-expect-error + $$id: widget.$$id, + facetName: 'key', + facetValue: 'value', + maxRecommendations: 10, + threshold: 95, + queryParameters: { userToken: 'token' }, + fallbackParameters: {}, + }) + ); + }); + }); +}); diff --git a/packages/instantsearch.js/src/connectors/trending-items/connectTrendingItems.ts b/packages/instantsearch.js/src/connectors/trending-items/connectTrendingItems.ts index ed51acf011..3e44e2bb1d 100644 --- a/packages/instantsearch.js/src/connectors/trending-items/connectTrendingItems.ts +++ b/packages/instantsearch.js/src/connectors/trending-items/connectTrendingItems.ts @@ -4,9 +4,18 @@ import { noop, escapeHits, TAG_PLACEHOLDER, + getObjectType, } from '../../lib/utils'; -import type { Connector, TransformItems, Hit, BaseHit } from '../../types'; +import type { + Connector, + TransformItems, + Hit, + BaseHit, + Renderer, + Unmounter, + UnknownWidgetParams, +} from '../../types'; import type { PlainSearchParameters, RecommendResultItem, @@ -17,14 +26,18 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); -export type TrendingItemsRenderState = { +export type TrendingItemsRenderState< + THit extends NonNullable = BaseHit +> = { /** * The matched recommendations from the Algolia API. */ items: Array>; }; -export type TrendingItemsConnectorParams = ( +export type TrendingItemsConnectorParams< + THit extends NonNullable = BaseHit +> = ( | { /** * The facet attribute to get recommendations for. @@ -35,7 +48,10 @@ export type TrendingItemsConnectorParams = ( */ facetValue: string; } - | { facetName?: never; facetValue?: never } + | { + facetName?: string; + facetValue?: string; + } ) & { /** * The number of recommendations to retrieve. @@ -71,106 +87,128 @@ export type TrendingItemsConnectorParams = ( transformItems?: TransformItems, { results: RecommendResultItem }>; }; -export type TrendingItemsWidgetDescription = { +export type TrendingItemsWidgetDescription< + THit extends NonNullable = BaseHit +> = { $$type: 'ais.trendingItems'; renderState: TrendingItemsRenderState; }; -export type TrendingItemsConnector = Connector< - TrendingItemsWidgetDescription, - TrendingItemsConnectorParams ->; - -const connectTrendingItems: TrendingItemsConnector = - function connectTrendingItems(renderFn, unmountFn = noop) { - checkRendering(renderFn, withUsage()); - - return function trendingItems(widgetParams) { - const { - facetName, - facetValue, - limit, - threshold, - fallbackParameters, - queryParameters, - // @MAJOR: this can default to false - escapeHTML = true, - transformItems = ((items) => items) as NonNullable< - TrendingItemsConnectorParams['transformItems'] - >, - } = widgetParams || {}; - - return { - dependsOn: 'recommend', - $$type: 'ais.trendingItems', - - init(initOptions) { - renderFn( - { - ...this.getWidgetRenderState(initOptions), - instantSearchInstance: initOptions.instantSearchInstance, - }, - true - ); - }, - - render(renderOptions) { - const renderState = this.getWidgetRenderState(renderOptions); - - renderFn( - { - ...renderState, - instantSearchInstance: renderOptions.instantSearchInstance, - }, - false - ); - }, - - getRenderState(renderState) { - return renderState; - }, - - getWidgetRenderState({ results }) { - if (results === null || results === undefined) { - return { items: [], widgetParams }; - } - - if (escapeHTML && results.hits.length > 0) { - results.hits = escapeHits(results.hits); - } - - return { - items: transformItems(results.hits, { - results: results as RecommendResultItem, - }), - widgetParams, - }; - }, - - dispose({ recommendState }) { - unmountFn(); - return recommendState.removeParams(this.$$id!); - }, - - getWidgetParameters(state) { - return state.removeParams(this.$$id!).addTrendingItems({ - facetName, - facetValue, - maxRecommendations: limit, - threshold, - fallbackParameters: { - ...fallbackParameters, - ...(escapeHTML ? TAG_PLACEHOLDER : {}), - }, - queryParameters: { - ...queryParameters, - ...(escapeHTML ? TAG_PLACEHOLDER : {}), - }, - $$id: this.$$id!, - }); - }, - }; +export type TrendingItemsConnector = BaseHit> = + Connector< + TrendingItemsWidgetDescription, + TrendingItemsConnectorParams + >; + +export default (function connectTrendingItems< + TWidgetParams extends UnknownWidgetParams +>( + renderFn: Renderer< + TrendingItemsRenderState, + TWidgetParams & TrendingItemsConnectorParams + >, + unmountFn: Unmounter = noop +) { + checkRendering(renderFn, withUsage()); + + return = BaseHit>( + widgetParams: TWidgetParams & TrendingItemsConnectorParams + ) => { + const { + facetName, + facetValue, + limit, + threshold, + fallbackParameters, + queryParameters, + // @MAJOR: this can default to false + escapeHTML = true, + transformItems = ((items) => items) as NonNullable< + TrendingItemsConnectorParams['transformItems'] + >, + } = widgetParams || {}; + + if ((facetName && !facetValue) || (!facetName && facetValue)) { + throw new Error( + withUsage( + `When you provide facetName (received type ${getObjectType( + facetName + )}), you must also provide facetValue (received type ${getObjectType( + facetValue + )}).` + ) + ); + } + + return { + dependsOn: 'recommend', + $$type: 'ais.trendingItems', + + init(initOptions) { + renderFn( + { + ...this.getWidgetRenderState(initOptions), + instantSearchInstance: initOptions.instantSearchInstance, + }, + true + ); + }, + + render(renderOptions) { + const renderState = this.getWidgetRenderState(renderOptions); + + renderFn( + { + ...renderState, + instantSearchInstance: renderOptions.instantSearchInstance, + }, + false + ); + }, + + getRenderState(renderState) { + return renderState; + }, + + getWidgetRenderState({ results }) { + if (results === null || results === undefined) { + return { items: [], widgetParams }; + } + + if (escapeHTML && results.hits.length > 0) { + results.hits = escapeHits(results.hits); + } + + return { + items: transformItems(results.hits, { + results: results as RecommendResultItem, + }), + widgetParams, + }; + }, + + dispose({ recommendState }) { + unmountFn(); + return recommendState.removeParams(this.$$id!); + }, + + getWidgetParameters(state) { + return state.removeParams(this.$$id!).addTrendingItems({ + facetName, + facetValue, + maxRecommendations: limit, + threshold, + fallbackParameters: { + ...fallbackParameters, + ...(escapeHTML ? TAG_PLACEHOLDER : {}), + }, + queryParameters: { + ...queryParameters, + ...(escapeHTML ? TAG_PLACEHOLDER : {}), + }, + $$id: this.$$id!, + }); + }, }; }; - -export default connectTrendingItems; +} satisfies TrendingItemsConnector); diff --git a/packages/instantsearch.js/src/lib/InstantSearch.ts b/packages/instantsearch.js/src/lib/InstantSearch.ts index eafa8ffed3..3a1d6f5bfd 100644 --- a/packages/instantsearch.js/src/lib/InstantSearch.ts +++ b/packages/instantsearch.js/src/lib/InstantSearch.ts @@ -7,7 +7,7 @@ import { isMetadataEnabled, } from '../middlewares/createMetadataMiddleware'; import { createRouterMiddleware } from '../middlewares/createRouterMiddleware'; -import index from '../widgets/index/index'; +import { index } from '../widgets'; import createHelpers from './createHelpers'; import { @@ -32,6 +32,7 @@ import type { InsightsClient as AlgoliaInsightsClient, SearchClient, Widget, + IndexWidget, UiState, CreateURL, Middleware, @@ -39,7 +40,6 @@ import type { RenderState, InitialResults, } from '../types'; -import type { IndexWidget } from '../widgets/index/index'; import type { AlgoliaSearchHelper } from 'algoliasearch-helper'; const withUsage = createDocumentationMessageGenerator({ diff --git a/packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx b/packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx index cc05a7534c..de155aecb8 100644 --- a/packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx +++ b/packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx @@ -30,8 +30,7 @@ import type { SearchBoxWidgetDescription, SearchBoxConnectorParams, } from '../../connectors/search-box/connectSearchBox'; -import type { UiState, Widget } from '../../types'; -import type { IndexWidget } from '../../widgets/index/index'; +import type { UiState, Widget, IndexWidget } from '../../types'; import type { RefObject } from 'preact'; type SearchBoxWidgetInstance = Widget< 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 8d1d1a022e..baf7e1504d 100644 --- a/packages/instantsearch.js/src/lib/utils/__tests__/resolveSearchParameters-test.ts +++ b/packages/instantsearch.js/src/lib/utils/__tests__/resolveSearchParameters-test.ts @@ -1,5 +1,5 @@ import { createIndexInitOptions } from '../../../../test/createWidget'; -import index from '../../../widgets/index/index'; +import { index } from '../../../widgets'; import { resolveSearchParameters } from '../resolveSearchParameters'; describe('mergeSearchParameters', () => { diff --git a/packages/instantsearch.js/src/lib/utils/checkIndexUiState.ts b/packages/instantsearch.js/src/lib/utils/checkIndexUiState.ts index 7bba729581..a9c468731d 100644 --- a/packages/instantsearch.js/src/lib/utils/checkIndexUiState.ts +++ b/packages/instantsearch.js/src/lib/utils/checkIndexUiState.ts @@ -2,8 +2,7 @@ import { capitalize } from './capitalize'; import { warning } from './logger'; import { keys } from './typedObject'; -import type { Widget, IndexUiState } from '../../types'; -import type { IndexWidget } from '../../widgets/index/index'; +import type { Widget, IndexUiState, IndexWidget } from '../../types'; // Some connectors are responsible for multiple widgets so we need // to map them. diff --git a/packages/instantsearch.js/src/lib/utils/checkRendering.ts b/packages/instantsearch.js/src/lib/utils/checkRendering.ts index e9ced36822..e6d91aceaa 100644 --- a/packages/instantsearch.js/src/lib/utils/checkRendering.ts +++ b/packages/instantsearch.js/src/lib/utils/checkRendering.ts @@ -2,10 +2,10 @@ import { getObjectType } from './getObjectType'; import type { Renderer } from '../../types/connector'; -export function checkRendering( - rendering: Renderer, +export function checkRendering( + rendering: any, usage: string -): void { +): asserts rendering is Renderer { if (rendering === undefined || typeof rendering !== 'function') { throw new Error(`The render function is not valid (received type ${getObjectType( rendering diff --git a/packages/instantsearch.js/src/lib/utils/getRefinements.ts b/packages/instantsearch.js/src/lib/utils/getRefinements.ts index ed9be7eb69..2f36eea75d 100644 --- a/packages/instantsearch.js/src/lib/utils/getRefinements.ts +++ b/packages/instantsearch.js/src/lib/utils/getRefinements.ts @@ -64,7 +64,7 @@ function getRefinement( name, escapedValue: escapeFacetValue(name), }; - let facet: any = find( + let facet: any = find( resultsFacets, (resultsFacet) => resultsFacet.name === attribute ); diff --git a/packages/instantsearch.js/src/lib/utils/getWidgetAttribute.ts b/packages/instantsearch.js/src/lib/utils/getWidgetAttribute.ts index 929616c6e2..153c99bc4e 100644 --- a/packages/instantsearch.js/src/lib/utils/getWidgetAttribute.ts +++ b/packages/instantsearch.js/src/lib/utils/getWidgetAttribute.ts @@ -1,5 +1,4 @@ -import type { InitOptions, Widget } from '../../types'; -import type { IndexWidget } from '../../widgets/index/index'; +import type { InitOptions, Widget, IndexWidget } from '../../types'; export function getWidgetAttribute( widget: Widget | IndexWidget, diff --git a/packages/instantsearch.js/src/lib/utils/isIndexWidget.ts b/packages/instantsearch.js/src/lib/utils/isIndexWidget.ts index 5d3d2e20ed..eeb74b4611 100644 --- a/packages/instantsearch.js/src/lib/utils/isIndexWidget.ts +++ b/packages/instantsearch.js/src/lib/utils/isIndexWidget.ts @@ -1,5 +1,4 @@ -import type { Widget } from '../../types'; -import type { IndexWidget } from '../../widgets/index/index'; +import type { Widget, IndexWidget } from '../../types'; export function isIndexWidget( widget: Widget | IndexWidget diff --git a/packages/instantsearch.js/src/lib/utils/render-args.ts b/packages/instantsearch.js/src/lib/utils/render-args.ts index aa467d2372..c08cc7ad76 100644 --- a/packages/instantsearch.js/src/lib/utils/render-args.ts +++ b/packages/instantsearch.js/src/lib/utils/render-args.ts @@ -1,5 +1,4 @@ -import type { InstantSearch, UiState, Widget } from '../../types'; -import type { IndexWidget } from '../../widgets/index/index'; +import type { InstantSearch, UiState, Widget, IndexWidget } from '../../types'; export function createInitArgs( instantSearchInstance: InstantSearch, diff --git a/packages/instantsearch.js/src/lib/utils/resolveSearchParameters.ts b/packages/instantsearch.js/src/lib/utils/resolveSearchParameters.ts index bfa7735983..025febb228 100644 --- a/packages/instantsearch.js/src/lib/utils/resolveSearchParameters.ts +++ b/packages/instantsearch.js/src/lib/utils/resolveSearchParameters.ts @@ -1,4 +1,4 @@ -import type { IndexWidget } from '../../widgets/index/index'; +import type { IndexWidget } from '../../types'; import type { SearchParameters } from 'algoliasearch-helper'; export function resolveSearchParameters( diff --git a/packages/instantsearch.js/src/lib/utils/setIndexHelperState.ts b/packages/instantsearch.js/src/lib/utils/setIndexHelperState.ts index 198bdafa28..70894f87d7 100644 --- a/packages/instantsearch.js/src/lib/utils/setIndexHelperState.ts +++ b/packages/instantsearch.js/src/lib/utils/setIndexHelperState.ts @@ -1,8 +1,7 @@ import { checkIndexUiState } from './checkIndexUiState'; import { isIndexWidget } from './isIndexWidget'; -import type { UiState } from '../../types'; -import type { IndexWidget } from '../../widgets/index/index'; +import type { UiState, IndexWidget } from '../../types'; export function setIndexHelperState( finalUiState: TUiState, diff --git a/packages/instantsearch.js/src/middlewares/createMetadataMiddleware.ts b/packages/instantsearch.js/src/middlewares/createMetadataMiddleware.ts index 2f0de22ccb..426ff83bb2 100644 --- a/packages/instantsearch.js/src/middlewares/createMetadataMiddleware.ts +++ b/packages/instantsearch.js/src/middlewares/createMetadataMiddleware.ts @@ -1,7 +1,11 @@ import { createInitArgs, safelyRunOnBrowser } from '../lib/utils'; -import type { InstantSearch, InternalMiddleware, Widget } from '../types'; -import type { IndexWidget } from '../widgets/index/index'; +import type { + InstantSearch, + InternalMiddleware, + Widget, + IndexWidget, +} from '../types'; type WidgetMetadata = | { diff --git a/packages/instantsearch.js/src/types/index.ts b/packages/instantsearch.js/src/types/index.ts index b008be0d91..46af57f9f5 100644 --- a/packages/instantsearch.js/src/types/index.ts +++ b/packages/instantsearch.js/src/types/index.ts @@ -22,6 +22,3 @@ export * from './widget'; export * from './ui-state'; export * from './render-state'; export * from './templates'; - -// from specific widgets -export type { IndexWidget } from '../widgets/index/index'; diff --git a/packages/instantsearch.js/src/types/results.ts b/packages/instantsearch.js/src/types/results.ts index 0957473a96..9cca49b438 100644 --- a/packages/instantsearch.js/src/types/results.ts +++ b/packages/instantsearch.js/src/types/results.ts @@ -40,38 +40,42 @@ export type GeoLoc = { lng: number; }; -export type AlgoliaHit> = { - objectID: string; - _highlightResult?: HitHighlightResult; - _snippetResult?: HitSnippetResult; - _rankingInfo?: { - promoted: boolean; - nbTypos: number; - firstMatchedWord: number; - proximityDistance?: number; - geoDistance: number; - geoPrecision?: number; - nbExactWords: number; - words: number; - filters: number; - userScore: number; - matchedGeoLocation?: { - lat: number; - lng: number; - distance: number; +export type AlgoliaHit = Record> = + { + objectID: string; + _highlightResult?: HitHighlightResult; + _snippetResult?: HitSnippetResult; + _rankingInfo?: { + promoted: boolean; + nbTypos: number; + firstMatchedWord: number; + proximityDistance?: number; + geoDistance: number; + geoPrecision?: number; + nbExactWords: number; + words: number; + filters: number; + userScore: number; + matchedGeoLocation?: { + lat: number; + lng: number; + distance: number; + }; }; - }; - _distinctSeqID?: number; - _geoloc?: GeoLoc; -} & THit; + _distinctSeqID?: number; + _geoloc?: GeoLoc; + } & THit; -export type BaseHit = Record; +export type BaseHit = Record; -export type Hit> = { +export type Hit = Record> = { __position: number; __queryID?: string; } & AlgoliaHit; +export type GeoHit = BaseHit> = Hit & + Required>; + /** * @deprecated use Hit[] directly instead */ diff --git a/packages/instantsearch.js/src/types/widget.ts b/packages/instantsearch.js/src/types/widget.ts index 6527903089..3888b0a06a 100644 --- a/packages/instantsearch.js/src/types/widget.ts +++ b/packages/instantsearch.js/src/types/widget.ts @@ -1,4 +1,4 @@ -import type { IndexWidget } from '../widgets/index/index'; +import type { IndexWidget } from '../widgets'; import type { InstantSearch } from './instantsearch'; import type { IndexRenderState, WidgetRenderState } from './render-state'; import type { IndexUiState, UiState } from './ui-state'; @@ -336,6 +336,8 @@ export type Widget< > & (SearchWidget | RecommendWidget); +export type { IndexWidget } from '../widgets'; + export type TransformItemsMetadata = { results?: SearchResults; }; diff --git a/packages/instantsearch.js/src/widgets/frequently-bought-together/frequently-bought-together.tsx b/packages/instantsearch.js/src/widgets/frequently-bought-together/frequently-bought-together.tsx index 2fd4c9b52d..ee8e132d84 100644 --- a/packages/instantsearch.js/src/widgets/frequently-bought-together/frequently-bought-together.tsx +++ b/packages/instantsearch.js/src/widgets/frequently-bought-together/frequently-bought-together.tsx @@ -17,7 +17,13 @@ import type { FrequentlyBoughtTogetherRenderState, } from '../../connectors/frequently-bought-together/connectFrequentlyBoughtTogether'; import type { PreparedTemplateProps } from '../../lib/templating'; -import type { Template, WidgetFactory, Hit, Renderer } from '../../types'; +import type { + Template, + WidgetFactory, + Hit, + Renderer, + BaseHit, +} from '../../types'; import type { RecommendResultItem } from 'algoliasearch-helper'; import type { RecommendClassNames, @@ -34,7 +40,7 @@ const FrequentlyBoughtTogether = createFrequentlyBoughtTogetherComponent({ }); const renderer = - ({ + = BaseHit>({ renderState, cssClasses, containerNode, @@ -44,10 +50,10 @@ const renderer = cssClasses: FrequentlyBoughtTogetherCSSClasses; renderState: { templateProps?: PreparedTemplateProps< - Required + Required> >; }; - templates: FrequentlyBoughtTogetherTemplates; + templates: FrequentlyBoughtTogetherTemplates; }): Renderer< FrequentlyBoughtTogetherRenderState, Partial @@ -55,8 +61,9 @@ const renderer = ({ items, results, instantSearchInstance }, isFirstRendering) => { if (isFirstRendering) { renderState.templateProps = prepareTemplateProps({ - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - defaultTemplates: {} as Required, + defaultTemplates: {} as unknown as Required< + FrequentlyBoughtTogetherTemplates + >, templatesConfig: instantSearchInstance.templatesConfig, templates, }); @@ -120,11 +127,13 @@ const renderer = export type FrequentlyBoughtTogetherCSSClasses = Partial; -export type FrequentlyBoughtTogetherTemplates = Partial<{ +export type FrequentlyBoughtTogetherTemplates< + THit extends NonNullable = BaseHit +> = Partial<{ /** * Template to use when there are no results. */ - empty: Template; + empty: Template>>; /** * Template to use for the header of the widget. @@ -132,7 +141,9 @@ export type FrequentlyBoughtTogetherTemplates = Partial<{ header: Template< Pick< Parameters< - NonNullable['headerComponent']> + NonNullable< + FrequentlyBoughtTogetherUiProps>['headerComponent'] + > >[0], 'items' > & { cssClasses: RecommendClassNames } @@ -141,10 +152,12 @@ export type FrequentlyBoughtTogetherTemplates = Partial<{ /** * Template to use for each result. This template will receive an object containing a single record. */ - item: Template; + item: Template>; }>; -type FrequentlyBoughtTogetherWidgetParams = { +type FrequentlyBoughtTogetherWidgetParams< + THit extends NonNullable = BaseHit +> = { /** * CSS Selector or HTMLElement to insert the widget. */ @@ -153,7 +166,7 @@ type FrequentlyBoughtTogetherWidgetParams = { /** * Templates to use for the widget. */ - templates?: FrequentlyBoughtTogetherTemplates; + templates?: FrequentlyBoughtTogetherTemplates; /** * CSS classes to add. @@ -169,48 +182,49 @@ export type FrequentlyBoughtTogetherWidget = WidgetFactory< FrequentlyBoughtTogetherWidgetParams >; -const frequentlyBoughtTogether: FrequentlyBoughtTogetherWidget = - function frequentlyBoughtTogether(widgetParams) { - const { - container, +export default (function frequentlyBoughtTogether< + THit extends NonNullable = BaseHit +>( + widgetParams: FrequentlyBoughtTogetherWidgetParams & + FrequentlyBoughtTogetherConnectorParams +) { + const { + container, + objectIDs, + limit, + queryParameters, + threshold, + escapeHTML, + transformItems, + templates = {}, + cssClasses = {}, + } = widgetParams || {}; + + if (!container) { + throw new Error(withUsage('The `container` option is required.')); + } + + const containerNode = getContainerNode(container); + + const specializedRenderer = renderer({ + containerNode, + cssClasses, + renderState: {}, + templates, + }); + + const makeWidget = connectFrequentlyBoughtTogether(specializedRenderer, () => + render(null, containerNode) + ); + return { + ...makeWidget({ objectIDs, limit, queryParameters, threshold, escapeHTML, transformItems, - templates = {}, - cssClasses = {}, - } = widgetParams || {}; - - if (!container) { - throw new Error(withUsage('The `container` option is required.')); - } - - const containerNode = getContainerNode(container); - - const specializedRenderer = renderer({ - containerNode, - cssClasses, - renderState: {}, - templates, - }); - - const makeWidget = connectFrequentlyBoughtTogether( - specializedRenderer, - () => render(null, containerNode) - ); - return { - ...makeWidget({ - objectIDs, - limit, - queryParameters, - threshold, - escapeHTML, - transformItems, - }), - $$widgetType: 'ais.frequentlyBoughtTogether', - }; + }), + $$widgetType: 'ais.frequentlyBoughtTogether', }; - -export default frequentlyBoughtTogether; +} satisfies FrequentlyBoughtTogetherWidget); diff --git a/packages/instantsearch.js/src/widgets/geo-search/__tests__/geo-search-test.ts b/packages/instantsearch.js/src/widgets/geo-search/__tests__/geo-search-test.ts index 22c60a84e8..67f4afb986 100644 --- a/packages/instantsearch.js/src/widgets/geo-search/__tests__/geo-search-test.ts +++ b/packages/instantsearch.js/src/widgets/geo-search/__tests__/geo-search-test.ts @@ -176,7 +176,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ instantSearchInstance, helper, @@ -184,7 +184,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -225,7 +225,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -233,7 +233,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -266,7 +266,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -274,7 +274,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -307,7 +307,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -345,7 +345,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -376,7 +376,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -384,7 +384,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -398,7 +398,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect(render).toHaveBeenCalledTimes(1); - widget.dispose!( + widget.dispose( createDisposeOptions({ helper, state: helper.state, @@ -422,7 +422,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -475,7 +475,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -513,7 +513,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -555,7 +555,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -590,7 +590,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -605,7 +605,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect.any(Function) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -638,7 +638,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -653,7 +653,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect.any(Function) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -685,7 +685,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ enableRefineOnMapMove: false, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -700,7 +700,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect.any(Function) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -731,7 +731,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -775,7 +775,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ enableRefine: false, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -817,7 +817,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -825,7 +825,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -867,7 +867,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -875,7 +875,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -926,7 +926,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -934,7 +934,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -989,7 +989,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ customHTMLMarker: true, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -997,7 +997,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1075,7 +1075,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1083,7 +1083,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1144,7 +1144,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1152,7 +1152,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1212,7 +1212,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1220,7 +1220,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1307,7 +1307,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1315,7 +1315,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1371,7 +1371,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1379,7 +1379,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1418,7 +1418,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1426,7 +1426,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1455,7 +1455,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1463,7 +1463,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1481,7 +1481,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ (googleReference.maps.Marker as unknown as jest.Mock).mockClear(); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1522,7 +1522,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1530,7 +1530,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1548,7 +1548,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ (googleReference.maps.Marker as unknown as jest.Mock).mockClear(); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1581,7 +1581,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1589,7 +1589,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1611,7 +1611,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ ); expect(renderer).toHaveBeenCalledTimes(2); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1664,7 +1664,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1674,7 +1674,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ simulateMapReadyEvent(googleReference); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1716,7 +1716,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ // Simulate refine simulateEvent(mapInstance, 'idle'); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1762,7 +1762,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1770,7 +1770,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1793,7 +1793,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ ); expect(renderer).toHaveBeenCalledTimes(2); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1832,7 +1832,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1840,7 +1840,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1858,7 +1858,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect(renderer).toHaveBeenCalledTimes(2); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1899,7 +1899,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1907,7 +1907,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }) ); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1924,7 +1924,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect(mapInstance.setCenter).toHaveBeenCalledTimes(0); expect(renderer).toHaveBeenCalledTimes(2); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, @@ -1962,7 +1962,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ container, }); - widget.init!( + widget.init( createInitOptions({ helper, instantSearchInstance, @@ -1972,7 +1972,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ simulateMapReadyEvent(googleReference); - widget.render!( + widget.render( createRenderOptions({ helper, instantSearchInstance, diff --git a/packages/instantsearch.js/src/widgets/geo-search/geo-search.ts b/packages/instantsearch.js/src/widgets/geo-search/geo-search.ts index 4566354c0f..1a3be91503 100644 --- a/packages/instantsearch.js/src/widgets/geo-search/geo-search.ts +++ b/packages/instantsearch.js/src/widgets/geo-search/geo-search.ts @@ -31,9 +31,9 @@ export type CreateMarker = (args: { const withUsage = createDocumentationMessageGenerator({ name: 'geo-search' }); const suit = component('GeoSearch'); -export type GeoSearchTemplates = Partial<{ +export type GeoSearchTemplates = Partial<{ /** Template to use for the marker. */ - HTMLMarker: Template; + HTMLMarker: Template; /** Template for the reset button. */ reset: Template; /** Template for the toggle label. */ @@ -65,13 +65,13 @@ export type GeoSearchCSSClasses = Partial<{ reset: string | string[]; }>; -export type GeoSearchMarker = { +export type GeoSearchMarker = { /** * Function used to create the options passed to the Google Maps marker. * See the documentation for more information: * https://developers.google.com/maps/documentation/javascript/reference/3/#MarkerOptions */ - createOptions?: (item: GeoHit) => TOptions; + createOptions?: (item: THit) => TOptions; /** * Object that takes an event type (ex: `click`, `mouseover`) as key and a * listener as value. The listener is provided with an object that contains: @@ -87,7 +87,7 @@ export type GeoSearchMarker = { }; }; -export type GeoSearchWidgetParams = { +export type GeoSearchWidgetParams = { /** * By default the map will set the zoom accordingly to the markers displayed on it. * When we refine it may happen that the results are empty. For those situations we @@ -104,7 +104,7 @@ export type GeoSearchWidgetParams = { */ initialPosition?: GeoLoc; /** Templates to use for the widget. */ - templates?: GeoSearchTemplates; + templates?: GeoSearchTemplates; /** CSS classes to add to the wrapping elements. */ cssClasses?: GeoSearchCSSClasses; /** @@ -172,7 +172,9 @@ export type GeoSearchWidget = WidgetFactory< * * Don't forget to explicitly set the `height` of the map container (default class `.ais-geo-search--map`), otherwise it won't be shown (it's a requirement of Google Maps). */ -const geoSearch: GeoSearchWidget = (widgetParams) => { +export default (function geoSearch( + widgetParams: GeoSearchWidgetParams & GeoSearchConnectorParams +) { const { initialZoom = 1, initialPosition = { lat: 0, lng: 0 }, @@ -229,7 +231,7 @@ const geoSearch: GeoSearchWidget = (widgetParams) => { reset: cx(suit({ descendantName: 'reset' }), userCssClasses.reset), }; - const templates = { + const templates: GeoSearchTemplates = { ...defaultTemplates, ...userTemplates, }; @@ -286,14 +288,16 @@ const geoSearch: GeoSearchWidget = (widgetParams) => { ); return { - ...makeWidget({ + ...makeWidget({ ...otherWidgetParams, + // @TODO: this type doesn't preserve the generic correctly, + // (but as they're internal only it's not a big problem) + templates: templates as any, renderState: {}, container: containerNode, googleReference, initialZoom, initialPosition, - templates, cssClasses, createMarker, markerOptions, @@ -303,6 +307,4 @@ const geoSearch: GeoSearchWidget = (widgetParams) => { }), $$widgetType: 'ais.geoSearch', }; -}; - -export default geoSearch; +} satisfies GeoSearchWidget); diff --git a/packages/instantsearch.js/src/widgets/hits/defaultTemplates.ts b/packages/instantsearch.js/src/widgets/hits/defaultTemplates.ts index 4ac431297d..a2f32d90cd 100644 --- a/packages/instantsearch.js/src/widgets/hits/defaultTemplates.ts +++ b/packages/instantsearch.js/src/widgets/hits/defaultTemplates.ts @@ -1,14 +1,16 @@ import { omit } from '../../lib/utils'; +// false positive lint error +// eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { HitsTemplates } from './hits'; -const defaultTemplates: HitsTemplates = { +const defaultTemplates = { empty() { return 'No results'; }, item(data) { return JSON.stringify(omit(data, ['__hitIndex']), null, 2); }, -}; +} satisfies HitsTemplates; export default defaultTemplates; diff --git a/packages/instantsearch.js/src/widgets/hits/hits.tsx b/packages/instantsearch.js/src/widgets/hits/hits.tsx index 642892434e..6aacacf9b1 100644 --- a/packages/instantsearch.js/src/widgets/hits/hits.tsx +++ b/packages/instantsearch.js/src/widgets/hits/hits.tsx @@ -28,6 +28,7 @@ import type { Hit, WidgetFactory, Renderer, + BaseHit, } from '../../types'; import type { SearchResults } from 'algoliasearch-helper'; import type { @@ -40,7 +41,7 @@ const withUsage = createDocumentationMessageGenerator({ name: 'hits' }); const Hits = createHitsComponent({ createElement: h, Fragment }); const renderer = - ({ + = BaseHit>({ renderState, cssClasses, containerNode, @@ -49,9 +50,9 @@ const renderer = containerNode: HTMLElement; cssClasses: HitsCSSClasses; renderState: { - templateProps?: PreparedTemplateProps; + templateProps?: PreparedTemplateProps>; }; - templates: HitsTemplates; + templates: HitsTemplates; }): Renderer> => ( { @@ -66,7 +67,7 @@ const renderer = isFirstRendering ) => { if (isFirstRendering) { - renderState.templateProps = prepareTemplateProps({ + renderState.templateProps = prepareTemplateProps>({ defaultTemplates, templatesConfig: instantSearchInstance.templatesConfig, templates, @@ -155,36 +156,37 @@ const renderer = export type HitsCSSClasses = Partial; -export type HitsTemplates = Partial<{ - /** - * Template to use when there are no results. - * - * @default 'No Results' - */ - empty: Template; - - /** - * Template to use for each result. This template will receive an object containing a single record. - * - * @default '' - */ - item: TemplateWithBindEvent< - Hit & { - /** @deprecated the index in the hits array, use __position instead, which is the absolute position */ - __hitIndex: number; - } - >; - - /** - * Template to use for the banner. - */ - banner: Template<{ - banner: Required; - className: string; +export type HitsTemplates = BaseHit> = + Partial<{ + /** + * Template to use when there are no results. + * + * @default 'No Results' + */ + empty: Template>; + + /** + * Template to use for each result. This template will receive an object containing a single record. + * + * @default '' + */ + item: TemplateWithBindEvent< + Hit & { + /** @deprecated the index in the hits array, use __position instead, which is the absolute position */ + __hitIndex: number; + } + >; + + /** + * Template to use for the banner. + */ + banner: Template<{ + banner: Required; + className: string; + }>; }>; -}>; -export type HitsWidgetParams = { +export type HitsWidgetParams = BaseHit> = { /** * CSS Selector or HTMLElement to insert the widget. */ @@ -193,7 +195,7 @@ export type HitsWidgetParams = { /** * Templates to use for the widget. */ - templates?: HitsTemplates; + templates?: HitsTemplates; /** * CSS classes to add. @@ -207,7 +209,9 @@ export type HitsWidget = WidgetFactory< HitsWidgetParams >; -const hits: HitsWidget = function hits(widgetParams) { +export default (function hits = BaseHit>( + widgetParams: HitsWidgetParams & HitsConnectorParams +) { const { container, escapeHTML, @@ -234,9 +238,10 @@ const hits: HitsWidget = function hits(widgetParams) { ); return { - ...makeWidget({ escapeHTML, transformItems }), + ...makeWidget({ + escapeHTML, + transformItems, + }), $$widgetType: 'ais.hits', }; -}; - -export default hits; +} satisfies HitsWidget); diff --git a/packages/instantsearch.js/src/widgets/index.ts b/packages/instantsearch.js/src/widgets/index.ts index 138a624851..bd65951466 100644 --- a/packages/instantsearch.js/src/widgets/index.ts +++ b/packages/instantsearch.js/src/widgets/index.ts @@ -27,6 +27,7 @@ export { default as hierarchicalMenu } from './hierarchical-menu/hierarchical-me export { default as hits } from './hits/hits'; export { default as hitsPerPage } from './hits-per-page/hits-per-page'; export { default as index } from './index/index'; +export type { IndexWidget } from './index/index'; export { default as infiniteHits } from './infinite-hits/infinite-hits'; export { default as menu } from './menu/menu'; export { default as menuSelect } from './menu-select/menu-select'; diff --git a/packages/instantsearch.js/src/widgets/infinite-hits/__tests__/infinite-hits-test.ts b/packages/instantsearch.js/src/widgets/infinite-hits/__tests__/infinite-hits-test.ts index f15e7998ac..75bcbda57d 100644 --- a/packages/instantsearch.js/src/widgets/infinite-hits/__tests__/infinite-hits-test.ts +++ b/packages/instantsearch.js/src/widgets/infinite-hits/__tests__/infinite-hits-test.ts @@ -68,7 +68,7 @@ describe('infiniteHits()', () => { cssClasses: { root: ['root', 'cx'] }, showPrevious: false, }); - widget.init!(createInitOptions({ helper })); + widget.init(createInitOptions({ helper })); results = new SearchResults(helper.state, [ createSingleSearchResponse({ hits: [{ objectID: '1' }, { objectID: '2' }], @@ -81,10 +81,10 @@ describe('infiniteHits()', () => { it('calls twice render(, container)', () => { const state = new SearchParameters({ page: 0 }); const instantSearchInstance = createInstantSearch(); - widget.init!(createInitOptions({ helper, instantSearchInstance })); + widget.init(createInitOptions({ helper, instantSearchInstance })); - widget.render!(createRenderOptions({ results, state })); - widget.render!(createRenderOptions({ results, state })); + widget.render(createRenderOptions({ results, state })); + widget.render(createRenderOptions({ results, state })); const firstRender = render.mock.calls[0][0] as VNode; const secondRender = render.mock.calls[1][0] as VNode; @@ -109,9 +109,9 @@ describe('infiniteHits()', () => { showPrevious: false, }); - widget.init!(createInitOptions({ helper })); + widget.init(createInitOptions({ helper })); - widget.render!( + widget.render( createRenderOptions({ results, state, @@ -124,9 +124,9 @@ describe('infiniteHits()', () => { }); it('if it is the last page, then the props should contain isLastPage true', () => { - widget.init!(createInitOptions({ helper })); + widget.init(createInitOptions({ helper })); const state1 = new SearchParameters({ page: 0 }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(state1, [ createSingleSearchResponse({ page: 0, nbPages: 2 }), @@ -135,7 +135,7 @@ describe('infiniteHits()', () => { }) ); const state2 = new SearchParameters({ page: 1 }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(state2, [ createSingleSearchResponse({ page: 1, nbPages: 2 }), @@ -163,9 +163,9 @@ describe('infiniteHits()', () => { const state = new SearchParameters({ page: 0 }); const instantSearchInstance = createInstantSearch(); - widget.init!(createInitOptions({ helper, instantSearchInstance })); + widget.init(createInitOptions({ helper, instantSearchInstance })); - widget.render!(createRenderOptions({ results, state })); + widget.render(createRenderOptions({ results, state })); const firstRender = render.mock.calls[0][0] as VNode; const { showMore } = firstRender.props as InfiniteHitsProps; @@ -185,8 +185,8 @@ describe('infiniteHits()', () => { hitsPerPage: 10, }), ]); - widget.init!(createInitOptions({ helper })); - widget.render!(createRenderOptions({ results, state })); + widget.init(createInitOptions({ helper })); + widget.render(createRenderOptions({ results, state })); expect(render).toHaveBeenCalledTimes(1); const firstRender = render.mock.calls[0][0] as VNode; @@ -196,11 +196,11 @@ describe('infiniteHits()', () => { }); it('if it is the first page, then the props should contain isFirstPage true', () => { - widget.init!(createInitOptions({ helper })); + widget.init(createInitOptions({ helper })); { const state = new SearchParameters({ page: 0 }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(state, [ createSingleSearchResponse({ page: state.page, nbPages: 2 }), @@ -212,7 +212,7 @@ describe('infiniteHits()', () => { { const state = new SearchParameters({ page: 1 }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(state, [ createSingleSearchResponse({ page: state.page, nbPages: 2 }), @@ -240,11 +240,11 @@ describe('infiniteHits()', () => { it('if it is not the first page, then the props should contain isFirstPage false', () => { helper.setPage(1); - widget.init!(createInitOptions({ helper })); + widget.init(createInitOptions({ helper })); const state = new SearchParameters({ page: 1 }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(state, [ createSingleSearchResponse({ page: state.page, nbPages: 2 }), @@ -296,14 +296,14 @@ describe('infiniteHits()', () => { showPrevious: false, cache: customCache, }); - widget.init!(createInitOptions({ helper })); + widget.init(createInitOptions({ helper })); expect(cachedState).toMatchInlineSnapshot(`undefined`); expect(cachedHits).toMatchInlineSnapshot(`undefined`); { const state = new SearchParameters({ page: 0, query: 'hello' }); - widget.render!( + widget.render( createRenderOptions({ results: new SearchResults(state, [ createSingleSearchResponse({ @@ -395,8 +395,8 @@ describe('infiniteHits()', () => { showPrevious: false, cache: customCache, }); - widget.init!(createInitOptions({ helper })); - widget.render!( + widget.init(createInitOptions({ helper })); + widget.render( createRenderOptions({ results: new SearchResults(state, [ createSingleSearchResponse({ 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 46f4b55294..85f7b21a6c 100644 --- a/packages/instantsearch.js/src/widgets/infinite-hits/infinite-hits.tsx +++ b/packages/instantsearch.js/src/widgets/infinite-hits/infinite-hits.tsx @@ -30,9 +30,10 @@ import type { WidgetFactory, Template, TemplateWithBindEvent, - Hit, InsightsClient, Renderer, + BaseHit, + Hit, } from '../../types'; import type { SearchResults } from 'algoliasearch-helper'; @@ -83,34 +84,37 @@ export type InfiniteHitsCSSClasses = Partial<{ disabledLoadMore: string | string[]; }>; -export type InfiniteHitsTemplates = Partial<{ - /** - * The template to use when there are no results. - */ - empty: Template; +export type InfiniteHitsTemplates = BaseHit> = + Partial<{ + /** + * The template to use when there are no results. + */ + empty: Template>; - /** - * The template to use for the “Show previous” label. - */ - showPreviousText: Template; + /** + * The template to use for the “Show previous” label. + */ + showPreviousText: Template; - /** - * The template to use for the “Show more” label. - */ - showMoreText: Template; + /** + * The template to use for the “Show more” label. + */ + showMoreText: Template; - /** - * The template to use for each result. - */ - item: TemplateWithBindEvent< - Hit & { - /** @deprecated the index in the hits array, use __position instead, which is the absolute position */ - __hitIndex: number; - } - >; -}>; + /** + * The template to use for each result. + */ + item: TemplateWithBindEvent< + Hit & { + /** @deprecated the index in the hits array, use __position instead, which is the absolute position */ + __hitIndex: number; + } + >; + }>; -export type InfiniteHitsWidgetParams = { +export type InfiniteHitsWidgetParams< + THit extends NonNullable = BaseHit +> = { /** * The CSS Selector or `HTMLElement` to insert the widget into. */ @@ -124,7 +128,7 @@ export type InfiniteHitsWidgetParams = { /** * The templates to use for the widget. */ - templates?: InfiniteHitsTemplates; + templates?: InfiniteHitsTemplates; /** * Reads and writes hits from/to cache. @@ -141,7 +145,7 @@ export type InfiniteHitsWidget = WidgetFactory< >; const renderer = - ({ + = BaseHit>({ containerNode, cssClasses, renderState, @@ -153,7 +157,7 @@ const renderer = renderState: { templateProps?: PreparedTemplateProps; }; - templates: InfiniteHitsTemplates; + templates: InfiniteHitsTemplates; showPrevious?: boolean; }): Renderer> => ( @@ -172,11 +176,12 @@ const renderer = isFirstRendering ) => { if (isFirstRendering) { - renderState.templateProps = prepareTemplateProps({ - defaultTemplates, - templatesConfig: instantSearchInstance.templatesConfig, - templates, - }); + renderState.templateProps = + prepareTemplateProps({ + defaultTemplates, + templatesConfig: instantSearchInstance.templatesConfig, + templates: templates as InfiniteHitsComponentTemplates, + }); return; } @@ -199,7 +204,12 @@ const renderer = ); }; -const infiniteHits: InfiniteHitsWidget = (widgetParams) => { +export default (function infiniteHits< + THit extends NonNullable = BaseHit +>( + widgetParams: InfiniteHitsWidgetParams & + InfiniteHitsConnectorParams +) { const { container, escapeHTML, @@ -257,6 +267,4 @@ const infiniteHits: InfiniteHitsWidget = (widgetParams) => { }), $$widgetType: 'ais.infiniteHits', }; -}; - -export default infiniteHits; +} satisfies InfiniteHitsWidget); 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 7724915fd1..c71ee1bc1a 100644 --- a/packages/instantsearch.js/src/widgets/looking-similar/looking-similar.tsx +++ b/packages/instantsearch.js/src/widgets/looking-similar/looking-similar.tsx @@ -17,7 +17,13 @@ import type { LookingSimilarRenderState, } from '../../connectors/looking-similar/connectLookingSimilar'; import type { PreparedTemplateProps } from '../../lib/templating'; -import type { Template, WidgetFactory, Hit, Renderer } from '../../types'; +import type { + Template, + WidgetFactory, + Hit, + Renderer, + BaseHit, +} from '../../types'; import type { RecommendResultItem } from 'algoliasearch-helper'; import type { RecommendClassNames, @@ -33,28 +39,27 @@ const LookingSimilar = createLookingSimilarComponent({ Fragment, }); -const renderer = - ({ - renderState, - cssClasses, - containerNode, - templates, - }: { - containerNode: HTMLElement; - cssClasses: LookingSimilarCSSClasses; - renderState: { - templateProps?: PreparedTemplateProps>; - }; - templates: LookingSimilarTemplates; - }): Renderer< - LookingSimilarRenderState, - Partial - > => - ({ items, results, instantSearchInstance }, isFirstRendering) => { +function createRenderer = BaseHit>({ + renderState, + cssClasses, + containerNode, + templates, +}: { + containerNode: HTMLElement; + cssClasses: LookingSimilarCSSClasses; + renderState: { + templateProps?: PreparedTemplateProps< + Required> + >; + }; + templates: LookingSimilarTemplates; +}): Renderer> { + return ({ items, results, instantSearchInstance }, isFirstRendering) => { if (isFirstRendering) { renderState.templateProps = prepareTemplateProps({ - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - defaultTemplates: {} as Required, + defaultTemplates: {} as unknown as Required< + LookingSimilarTemplates + >, templatesConfig: instantSearchInstance.templatesConfig, templates, }); @@ -115,21 +120,26 @@ const renderer = containerNode ); }; +} export type LookingSimilarCSSClasses = Partial; -export type LookingSimilarTemplates = Partial<{ +export type LookingSimilarTemplates< + THit extends NonNullable = BaseHit +> = Partial<{ /** * Template to use when there are no results. */ - empty: Template; + empty: Template>>; /** * Template to use for the header of the widget. */ header: Template< Pick< - Parameters['headerComponent']>>[0], + Parameters< + NonNullable>['headerComponent']> + >[0], 'items' > & { cssClasses: RecommendClassNames } >; @@ -137,10 +147,10 @@ export type LookingSimilarTemplates = Partial<{ /** * Template to use for each result. This template will receive an object containing a single record. */ - item: Template; + item: Template>; }>; -type LookingSimilarWidgetParams = { +type LookingSimilarWidgetParams = BaseHit> = { /** * CSS Selector or HTMLElement to insert the widget. */ @@ -149,7 +159,7 @@ type LookingSimilarWidgetParams = { /** * Templates to use for the widget. */ - templates?: LookingSimilarTemplates; + templates?: LookingSimilarTemplates; /** * CSS classes to add. @@ -165,8 +175,11 @@ export type LookingSimilarWidget = WidgetFactory< LookingSimilarWidgetParams >; -const lookingSimilar: LookingSimilarWidget = function lookingSimilar( - widgetParams +export default (function lookingSimilar< + THit extends NonNullable = BaseHit +>( + widgetParams: LookingSimilarWidgetParams & + LookingSimilarConnectorParams ) { const { container, @@ -187,7 +200,7 @@ const lookingSimilar: LookingSimilarWidget = function lookingSimilar( const containerNode = getContainerNode(container); - const specializedRenderer = renderer({ + const specializedRenderer = createRenderer({ containerNode, cssClasses, renderState: {}, @@ -209,6 +222,4 @@ const lookingSimilar: LookingSimilarWidget = function lookingSimilar( }), $$widgetType: 'ais.lookingSimilar', }; -}; - -export default lookingSimilar; +} satisfies LookingSimilarWidget); 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 13b3b05071..55359282b4 100644 --- a/packages/instantsearch.js/src/widgets/related-products/related-products.tsx +++ b/packages/instantsearch.js/src/widgets/related-products/related-products.tsx @@ -17,7 +17,13 @@ import type { RelatedProductsRenderState, } from '../../connectors/related-products/connectRelatedProducts'; import type { PreparedTemplateProps } from '../../lib/templating'; -import type { Template, WidgetFactory, Hit, Renderer } from '../../types'; +import type { + Template, + WidgetFactory, + Hit, + Renderer, + BaseHit, +} from '../../types'; import type { RecommendResultItem } from 'algoliasearch-helper'; import type { RecommendClassNames, @@ -33,21 +39,21 @@ const RelatedProducts = createRelatedProductsComponent({ Fragment, }); -type CreateRendererProps = { +type CreateRendererProps = BaseHit> = { containerNode: HTMLElement; cssClasses: RelatedProductsCSSClasses; renderState: { - templateProps?: PreparedTemplateProps; + templateProps?: PreparedTemplateProps>; }; - templates: RelatedProductsTemplates; + templates: RelatedProductsTemplates; }; -function createRenderer({ +function createRenderer = BaseHit>({ renderState, cssClasses, containerNode, templates, -}: CreateRendererProps): Renderer< +}: CreateRendererProps): Renderer< RelatedProductsRenderState, Partial > { @@ -57,8 +63,9 @@ function createRenderer({ ) { if (isFirstRendering) { renderState.templateProps = prepareTemplateProps({ - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - defaultTemplates: {} as RelatedProductsTemplates, + defaultTemplates: {} as unknown as Required< + RelatedProductsTemplates + >, templatesConfig: instantSearchInstance.templatesConfig, templates, }); @@ -127,11 +134,13 @@ function createRenderer({ export type RelatedProductsCSSClasses = Partial; -export type RelatedProductsTemplates = Partial<{ +export type RelatedProductsTemplates< + THit extends NonNullable = BaseHit +> = Partial<{ /** * Template to use when there are no results. */ - empty: Template; + empty: Template>>; /** * Template to use for the header of the widget. @@ -139,7 +148,7 @@ export type RelatedProductsTemplates = Partial<{ header: Template< Pick< Parameters< - NonNullable['headerComponent']> + NonNullable>['headerComponent']> >[0], 'items' > & { cssClasses: RecommendClassNames } @@ -148,10 +157,10 @@ export type RelatedProductsTemplates = Partial<{ /** * Template to use for each result. This template will receive an object containing a single record. */ - item: Template; + item: Template>; }>; -type RelatedProductsWidgetParams = { +type RelatedProductsWidgetParams = BaseHit> = { /** * CSS selector or `HTMLElement` to insert the widget into. */ @@ -160,7 +169,7 @@ type RelatedProductsWidgetParams = { /** * Templates to customize the widget. */ - templates?: RelatedProductsTemplates; + templates?: RelatedProductsTemplates; /** * CSS classes to add to the widget elements. @@ -176,8 +185,11 @@ export type RelatedProductsWidget = WidgetFactory< RelatedProductsWidgetParams >; -const relatedProducts: RelatedProductsWidget = function relatedProducts( - widgetParams +export default (function relatedProducts< + THit extends NonNullable = BaseHit +>( + widgetParams: RelatedProductsWidgetParams & + RelatedProductsConnectorParams ) { const { container, @@ -221,6 +233,4 @@ const relatedProducts: RelatedProductsWidget = function relatedProducts( }), $$widgetType: 'ais.relatedProducts', }; -}; - -export default relatedProducts; +} satisfies RelatedProductsWidget); 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 a10c3bfd89..842f3787e6 100644 --- a/packages/instantsearch.js/src/widgets/trending-items/trending-items.tsx +++ b/packages/instantsearch.js/src/widgets/trending-items/trending-items.tsx @@ -17,7 +17,13 @@ import type { TrendingItemsRenderState, } from '../../connectors/trending-items/connectTrendingItems'; import type { PreparedTemplateProps } from '../../lib/templating'; -import type { Template, WidgetFactory, Hit, Renderer } from '../../types'; +import type { + Template, + WidgetFactory, + Hit, + Renderer, + BaseHit, +} from '../../types'; import type { RecommendResultItem } from 'algoliasearch-helper'; import type { RecommendClassNames, @@ -33,21 +39,21 @@ const TrendingItems = createTrendingItemsComponent({ Fragment, }); -type CreateRendererProps = { +type CreateRendererProps = BaseHit> = { containerNode: HTMLElement; cssClasses: TrendingItemsCSSClasses; renderState: { - templateProps?: PreparedTemplateProps; + templateProps?: PreparedTemplateProps>; }; - templates: TrendingItemsTemplates; + templates: TrendingItemsTemplates; }; -function createRenderer({ +function createRenderer = BaseHit>({ renderState, cssClasses, containerNode, templates, -}: CreateRendererProps): Renderer< +}: CreateRendererProps): Renderer< TrendingItemsRenderState, Partial > { @@ -57,8 +63,9 @@ function createRenderer({ ) { if (isFirstRendering) { renderState.templateProps = prepareTemplateProps({ - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - defaultTemplates: {} as TrendingItemsTemplates, + defaultTemplates: {} as unknown as Required< + TrendingItemsTemplates + >, templatesConfig: instantSearchInstance.templatesConfig, templates, }); @@ -127,29 +134,32 @@ function createRenderer({ export type TrendingItemsCSSClasses = Partial; -export type TrendingItemsTemplates = Partial<{ - /** - * Template to use when there are no results. - */ - empty: Template; - - /** - * Template to use for the header of the widget. - */ - header: Template< - Pick< - Parameters['headerComponent']>>[0], - 'items' - > & { cssClasses: RecommendClassNames } - >; - - /** - * Template to use for each result. This template will receive an object containing a single record. - */ - item: Template; -}>; - -type TrendingItemsWidgetParams = { +export type TrendingItemsTemplates = BaseHit> = + Partial<{ + /** + * Template to use when there are no results. + */ + empty: Template>>; + + /** + * Template to use for the header of the widget. + */ + header: Template< + Pick< + Parameters< + NonNullable>['headerComponent']> + >[0], + 'items' + > & { cssClasses: RecommendClassNames } + >; + + /** + * Template to use for each result. This template will receive an object containing a single record. + */ + item: Template>; + }>; + +type TrendingItemsWidgetParams = BaseHit> = { /** * CSS selector or `HTMLElement` to insert the widget into. */ @@ -158,7 +168,7 @@ type TrendingItemsWidgetParams = { /** * Templates to customize the widget. */ - templates?: TrendingItemsTemplates; + templates?: TrendingItemsTemplates; /** * CSS classes to add to the widget elements. @@ -174,8 +184,11 @@ export type TrendingItemsWidget = WidgetFactory< TrendingItemsWidgetParams >; -const trendingItems: TrendingItemsWidget = function trendingItems( - widgetParams +export default (function trendingItems< + THit extends NonNullable = BaseHit +>( + widgetParams: TrendingItemsWidgetParams & + TrendingItemsConnectorParams ) { const { container, @@ -223,6 +236,4 @@ const trendingItems: TrendingItemsWidget = function trendingItems( }), $$widgetType: 'ais.trendingItems', }; -}; - -export default trendingItems; +} satisfies TrendingItemsWidget); diff --git a/packages/instantsearch.js/test/createInstantSearch.ts b/packages/instantsearch.js/test/createInstantSearch.ts index e7934d2442..89bcce40ab 100644 --- a/packages/instantsearch.js/test/createInstantSearch.ts +++ b/packages/instantsearch.js/test/createInstantSearch.ts @@ -3,7 +3,7 @@ import algoliasearchHelper from 'algoliasearch-helper'; import { INSTANTSEARCH_FUTURE_DEFAULTS } from '../src/lib/InstantSearch'; import { defer } from '../src/lib/utils'; -import index from '../src/widgets/index/index'; +import { index } from '../src/widgets'; import type { InstantSearch } from '../src/types'; diff --git a/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts b/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts index 0df48a6460..57b69fb7dc 100644 --- a/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts +++ b/packages/react-instantsearch-core/src/connectors/useGeoSearch.ts @@ -3,18 +3,17 @@ import connectGeoSearch from 'instantsearch.js/es/connectors/geo-search/connectG import { useConnector } from '../hooks/useConnector'; import type { AdditionalWidgetProperties } from '../hooks/useConnector'; -import type { BaseHit } from 'instantsearch.js'; +import type { GeoHit } from 'instantsearch.js'; import type { GeoSearchConnector, GeoSearchConnectorParams, GeoSearchWidgetDescription, } from 'instantsearch.js/es/connectors/geo-search/connectGeoSearch'; -export type { GeoHit } from 'instantsearch.js/es/connectors/geo-search/connectGeoSearch'; -export type UseGeoSearchProps = +export type UseGeoSearchProps = GeoSearchConnectorParams; -export function useGeoSearch( +export function useGeoSearch( props?: UseGeoSearchProps, additionalWidgetProperties?: AdditionalWidgetProperties ) { diff --git a/yarn.lock b/yarn.lock index 43dea6ce12..7d59cebb44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3648,47 +3648,48 @@ dependencies: sass "^1.59.3" -"@microsoft/api-extractor-model@7.13.3": - version "7.13.3" - resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.13.3.tgz#ac01c064c5af520d3661c85d7e5ef95e1ca8ab92" - integrity sha512-uXilAhu2GcvyY/0NwVRk3AN7TFYjkPnjHLV2UywTTz9uglS+Af0YjNrCy+aaK8qXtfbFWdBzkH9N2XU8/YBeRQ== - dependencies: - "@microsoft/tsdoc" "0.13.2" - "@microsoft/tsdoc-config" "~0.15.2" - "@rushstack/node-core-library" "3.39.0" - -"@microsoft/api-extractor@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.18.0.tgz#a39ad351269696736737557b839be1eb4b4f6941" - integrity sha512-n9vrK5t7ycaO3NSfQFae5resy555b1jBiTN+E4XMpCbuvIz5x0UX5xzIX7xs8Q4F7YmTV3QRe15wpa/gwbyyrA== - dependencies: - "@microsoft/api-extractor-model" "7.13.3" - "@microsoft/tsdoc" "0.13.2" - "@microsoft/tsdoc-config" "~0.15.2" - "@rushstack/node-core-library" "3.39.0" - "@rushstack/rig-package" "0.2.12" - "@rushstack/ts-command-line" "4.8.0" - colors "~1.2.1" +"@microsoft/api-extractor-model@7.28.21": + version "7.28.21" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.28.21.tgz#208810655ef5f5131c212cfb6c353ccc98cd1bf8" + integrity sha512-AZSdhK/vO4ddukfheXZmrkI5180XLeAqwzu/5pTsJHsXYSyNt3H3VJyynUYKMeNcveG9QLgljH3XRr/LqEfC0Q== + dependencies: + "@microsoft/tsdoc" "0.14.2" + "@microsoft/tsdoc-config" "~0.16.1" + "@rushstack/node-core-library" "5.3.0" + +"@microsoft/api-extractor@7.45.1": + version "7.45.1" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.45.1.tgz#6f06ff9e801802ee16cafb717f42dd6d675d2f4d" + integrity sha512-FZgcJxEmHA15gxTb1PpXHTforRcyIOLp6GKMqqk+ok8M58QJ0y54Bk+dcTcBC92nmanZppKn5/VRXPP4XvzB3Q== + dependencies: + "@microsoft/api-extractor-model" "7.28.21" + "@microsoft/tsdoc" "0.14.2" + "@microsoft/tsdoc-config" "~0.16.1" + "@rushstack/node-core-library" "5.3.0" + "@rushstack/rig-package" "0.5.2" + "@rushstack/terminal" "0.12.2" + "@rushstack/ts-command-line" "4.21.4" lodash "~4.17.15" - resolve "~1.17.0" - semver "~7.3.0" + minimatch "~3.0.3" + resolve "~1.22.1" + semver "~7.5.4" source-map "~0.6.1" - typescript "~4.3.2" + typescript "5.4.2" -"@microsoft/tsdoc-config@~0.15.2": - version "0.15.2" - resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz#eb353c93f3b62ab74bdc9ab6f4a82bcf80140f14" - integrity sha512-mK19b2wJHSdNf8znXSMYVShAHktVr/ib0Ck2FA3lsVBSEhSI/TfXT7DJQkAYgcztTuwazGcg58ZjYdk0hTCVrA== +"@microsoft/tsdoc-config@~0.16.1": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" + integrity sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw== dependencies: - "@microsoft/tsdoc" "0.13.2" + "@microsoft/tsdoc" "0.14.2" ajv "~6.12.6" jju "~1.4.0" resolve "~1.19.0" -"@microsoft/tsdoc@0.13.2": - version "0.13.2" - resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz#3b0efb6d3903bd49edb073696f60e90df08efb26" - integrity sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg== +"@microsoft/tsdoc@0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" + integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== "@mischnic/json-sourcemap@^0.1.0": version "0.1.0" @@ -5398,37 +5399,44 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== -"@rushstack/node-core-library@3.39.0": - version "3.39.0" - resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.39.0.tgz#38928946d15ae89b773386cf97433d0d1ec83b93" - integrity sha512-kgu3+7/zOBkZU0+NdJb1rcHcpk3/oTjn5c8cg5nUTn+JDjEw58yG83SoeJEcRNNdl11dGX0lKG2PxPsjCokZOQ== +"@rushstack/node-core-library@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-5.3.0.tgz#9ce1026fa39e953704eb9a4ba42cdcaed6a2d857" + integrity sha512-t23gjdZV6aWkbwXSE3TkKr1UXJFbXICvAOJ0MRQEB/ZYGhfSJqqrQFaGd20I1a/nIIHJEkNO0xzycHixjcbCPw== dependencies: - "@types/node" "10.17.13" - colors "~1.2.1" + ajv "~8.13.0" + ajv-draft-04 "~1.0.0" + ajv-formats "~3.0.1" fs-extra "~7.0.1" import-lazy "~4.0.0" jju "~1.4.0" - resolve "~1.17.0" - semver "~7.3.0" - timsort "~0.3.0" - z-schema "~3.18.3" + resolve "~1.22.1" + semver "~7.5.4" -"@rushstack/rig-package@0.2.12": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@rushstack/rig-package/-/rig-package-0.2.12.tgz#c434d62b28e0418a040938226f8913971d0424c7" - integrity sha512-nbePcvF8hQwv0ql9aeQxcaMPK/h1OLAC00W7fWCRWIvD2MchZOE8jumIIr66HGrfG2X1sw++m/ZYI4D+BM5ovQ== +"@rushstack/rig-package@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@rushstack/rig-package/-/rig-package-0.5.2.tgz#0e23a115904678717a74049661931c0b37dd5495" + integrity sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA== dependencies: - resolve "~1.17.0" + resolve "~1.22.1" strip-json-comments "~3.1.1" -"@rushstack/ts-command-line@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rushstack/ts-command-line/-/ts-command-line-4.8.0.tgz#611accb931b9ac62ff4d078f68f95c47f6606724" - integrity sha512-nZ8cbzVF1VmFPfSJfy8vEohdiFAH/59Y/Y+B4nsJbn4SkifLJ8LqNZ5+LxCC2UR242EXFumxlsY1d6fPBxck5Q== +"@rushstack/terminal@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@rushstack/terminal/-/terminal-0.12.2.tgz#c2a61d81333352ff39db399b6e17b689d8e83628" + integrity sha512-yaHKyD/l6Zg34pC5zzc/KdiRBHy8zAH7ZbL3umpDLnvTrZ0SP8MVYZu9xA2lRsGkKfGbv/6gQhyNq4/tRzXH4A== dependencies: + "@rushstack/node-core-library" "5.3.0" + supports-color "~8.1.1" + +"@rushstack/ts-command-line@4.21.4": + version "4.21.4" + resolved "https://registry.yarnpkg.com/@rushstack/ts-command-line/-/ts-command-line-4.21.4.tgz#3a4de7db1e312b4f11fb7e69166ce4cbeea7618b" + integrity sha512-3ZjQ11kpQwk/lDQqbmxC8UuU6yD20Sy4uNTWIaBEJ5474hEFEE4cbDOS4F9R4zcyCkkaQYv674K2QTunDF5dsQ== + dependencies: + "@rushstack/terminal" "0.12.2" "@types/argparse" "1.0.38" argparse "~1.0.9" - colors "~1.2.1" string-argv "~0.3.1" "@segment/loosely-validate-event@^2.0.0": @@ -6461,11 +6469,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.13.tgz#dff34f226ec1ac0432ae3b136ec5552bd3b9c0fe" integrity sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w== -"@types/node@10.17.13": - version "10.17.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" - integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== - "@types/node@17.0.40": version "17.0.40" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.40.tgz#76ee88ae03650de8064a6cf75b8d95f9f4a16090" @@ -7926,11 +7929,23 @@ airbnb-js-shims@^2.2.1: string.prototype.padstart "^3.0.0" symbol.prototype.description "^1.0.0" +ajv-draft-04@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" + integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw== + ajv-errors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" integrity sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk= +ajv-formats@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.0.0, ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -7956,15 +7971,25 @@ ajv@^6.0.1, ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@ json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1: - version "8.11.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.2.tgz#aecb20b50607acf2569b6382167b65a96008bb78" - integrity sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg== +ajv@^8.0.0, ajv@^8.0.1: + version "8.14.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.14.0.tgz#f514ddfd4756abb200e1704414963620a625ebbb" + integrity sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" + uri-js "^4.4.1" + +ajv@~8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91" + integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA== + dependencies: + fast-deep-equal "^3.1.3" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.4.1" algoliasearch-helper@3.14.0: version "3.14.0" @@ -11244,11 +11269,6 @@ colors@1.4.0, colors@^1.1.2, colors@^1.3.3, colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -colors@~1.2.1: - version "1.2.5" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc" - integrity sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg== - columnify@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3" @@ -11344,7 +11364,7 @@ commander@4.1.1, commander@^4.0.0, commander@^4.0.1, commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.7.1, commander@^2.9.0: +commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.9.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -15775,10 +15795,10 @@ fsevents@^2.1.2, fsevents@^2.3.2, fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -function-bind@^1.0.2, function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.0.2, function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.0, function.prototype.name@^1.1.2, function.prototype.name@^1.1.5: version "1.1.5" @@ -16734,6 +16754,13 @@ hashids@1.1.4: resolved "https://registry.yarnpkg.com/hashids/-/hashids-1.1.4.tgz#e4ff92ad66b684a3bd6aace7c17d66618ee5fa21" integrity sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w== +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-to-hyperscript@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz#3decd7cb4654bca8883f6fcbd4fb3695628c4296" @@ -18011,12 +18038,12 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" -is-core-module@^2.1.0, is-core-module@^2.11.0, is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.6.0, is-core-module@^2.8.0, is-core-module@^2.8.1: - version "2.12.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" - integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== +is-core-module@^2.1.0, is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.6.0, is-core-module@^2.8.0, is-core-module@^2.8.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" is-data-descriptor@^0.1.4: version "0.1.4" @@ -20457,12 +20484,7 @@ lodash.frompairs@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz#bc4e5207fa2757c136e573614e9664506b2b1bd2" integrity sha1-vE5SB/onV8E25XNhTpZkUGsrG9I= -lodash.get@^4.0.0: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash.isequal@^4.0.0, lodash.isequal@^4.5.0: +lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== @@ -22094,7 +22116,7 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@3.0.5, minimatch@~3.0.2: +minimatch@3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== @@ -22108,6 +22130,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@~3.0.2, minimatch@~3.0.3: + version "3.0.8" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -27325,12 +27354,12 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg== -resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.4.0: - version "1.22.2" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" - integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== +resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.4.0, resolve@~1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.11.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -27342,13 +27371,6 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" -resolve@~1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - resolve@~1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" @@ -28065,7 +28087,7 @@ semver@7.3.4: dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.5.2: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.5.2, semver@~7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -28077,7 +28099,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@~7.3.0, semver@~7.3.2: +semver@~7.3.2: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -29798,7 +29820,7 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@~8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -30370,7 +30392,7 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -timsort@^0.3.0, timsort@~0.3.0: +timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A== @@ -30890,7 +30912,12 @@ typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.7.tgz#799207136a37f3b3efb8c66c40010d032714dc73" integrity sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ== -typescript@*, typescript@5.1.3: +typescript@*, typescript@5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" + integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== + +typescript@5.1.3: version "5.1.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826" integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw== @@ -30900,11 +30927,6 @@ typescript@*, typescript@5.1.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== -typescript@~4.3.2: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== - typical@^2.4.2, typical@^2.6.0, typical@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" @@ -31399,10 +31421,10 @@ upper-case@^2.0.2: dependencies: tslib "^2.0.3" -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== +uri-js@^4.2.2, uri-js@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" @@ -31660,11 +31682,6 @@ validator@^12.0.0: resolved "https://registry.yarnpkg.com/validator/-/validator-12.2.0.tgz#660d47e96267033fd070096c3b1a6f2db4380a0a" integrity sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ== -validator@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9" - integrity sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA== - vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -33289,17 +33306,6 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -z-schema@~3.18.3: - version "3.18.4" - resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.18.4.tgz#ea8132b279533ee60be2485a02f7e3e42541a9a2" - integrity sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw== - dependencies: - lodash.get "^4.0.0" - lodash.isequal "^4.0.0" - validator "^8.0.0" - optionalDependencies: - commander "^2.7.1" - zen-observable-ts@^0.8.6: version "0.8.21" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d"