diff --git a/package.json b/package.json index b62a0d1ff..284806bd3 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,7 @@ "release": "shipjs prepare" }, "dependencies": { - "algoliasearch-helper": "^3.1.0", - "instantsearch.js": "^4.25.0" + "instantsearch.js": "^4.34.0" }, "peerDependencies": { "algoliasearch": ">= 3.32.0 < 5", @@ -67,6 +66,7 @@ "@wdio/spec-reporter": "^5.11.7", "@wdio/static-server-service": "^5.11.0", "algoliasearch": "4.0.1", + "algoliasearch-helper": "3.6.2", "babel-eslint": "10.0.1", "babel-jest": "23.6.0", "babel-preset-es2015": "6.24.1", @@ -114,7 +114,7 @@ "bundlesize": [ { "path": "./dist/vue-instantsearch.js", - "maxSize": "54 kB" + "maxSize": "56.50 kB" }, { "path": "./dist/vue-instantsearch.common.js", diff --git a/src/mixins/widget.js b/src/mixins/widget.js index 65424fec7..7551d43c4 100644 --- a/src/mixins/widget.js +++ b/src/mixins/widget.js @@ -30,7 +30,7 @@ export const createWidgetMixin = ({ connector } = {}) => ({ this.getParentIndex().addWidgets([this.widget]); if ( - this.instantSearchInstance.__initialSearchResults && + this.instantSearchInstance._initialResults && !this.instantSearchInstance.started ) { if (typeof this.instantSearchInstance.__forceRender !== 'function') { diff --git a/src/util/__tests__/createServerRootMixin.test.js b/src/util/__tests__/createServerRootMixin.test.js index 727331d5c..da07ea815 100644 --- a/src/util/__tests__/createServerRootMixin.test.js +++ b/src/util/__tests__/createServerRootMixin.test.js @@ -10,11 +10,7 @@ import SearchBox from '../../components/SearchBox.vue'; import { createWidgetMixin } from '../../mixins/widget'; import { createFakeClient } from '../testutils/client'; import { createSerializedState } from '../testutils/helper'; -import { - SearchResults, - SearchParameters, - AlgoliaSearchHelper, -} from 'algoliasearch-helper'; +import { SearchParameters, SearchResults } from 'algoliasearch-helper'; jest.unmock('instantsearch.js/es'); @@ -54,9 +50,11 @@ describe('createServerRootMixin', () => { }), ], }) - ).toThrowErrorMatchingInlineSnapshot( - `"createServerRootMixin requires \`searchClient\` and \`indexName\` in the first argument"` - ); + ).toThrowErrorMatchingInlineSnapshot(` +"The \`searchClient\` option is required. + +See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/" +`); }); it('requires indexName', () => { @@ -70,9 +68,11 @@ describe('createServerRootMixin', () => { }), ], }) - ).toThrowErrorMatchingInlineSnapshot( - `"createServerRootMixin requires \`searchClient\` and \`indexName\` in the first argument"` - ); + ).toThrowErrorMatchingInlineSnapshot(` +"The \`indexName\` option is required. + +See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/" +`); }); it('creates an instantsearch instance on "data"', () => { @@ -246,6 +246,7 @@ Array [ h(SearchBox), ]); }, + serverPrefetch() { return this.instantsearch.findResultsState(this); }, @@ -388,7 +389,7 @@ Array [ this.instantsearch.mainIndex.getWidgets().map(w => w.$$type) ).toEqual(['ais.configure']); - expect(res.hello._state.hitsPerPage).toBe(100); + expect(res.hello.state.hitsPerPage).toBe(100); }) // jest throws an error we need to catch, since stuck in the flow .catch(e => { @@ -423,7 +424,7 @@ Array [ expect.assertions(2); - const App = Vue.component('App', { + const App = { mixins: [ forceIsServerMixin, createServerRootMixin({ @@ -431,11 +432,8 @@ Array [ indexName: 'hello', }), ], - render(h) { - return h(InstantSearchSsr, {}, [ - this.$scopedSlots.default({ test: true }), - ]); - }, + render: h => + h(InstantSearchSsr, {}, [this.$scopedSlots.default({ test: true })]), serverPrefetch() { return ( this.instantsearch @@ -453,26 +451,23 @@ Array [ }) ); }, - }); + }; const wrapper = new Vue({ mixins: [forceIsServerMixin], - render(h) { - return h(App, { + render: h => + h(App, { scopedSlots: { default({ test }) { if (test) { return h(Configure, { - attrs: { - hitsPerPage: 100, - }, + hitsPerPage: 100, }); } return null; }, }, - }); - }, + }), }); await renderToString(wrapper); @@ -519,63 +514,76 @@ Array [ await renderToString(wrapper); }); - }); - - describe('hydrate', () => { - it('sets __initialSearchResults', () => { - const serialized = createSerializedState(); + it('searches only once', async () => { + const searchClient = createFakeClient(); const app = { mixins: [ + forceIsServerMixin, createServerRootMixin({ - searchClient: createFakeClient(), + searchClient, indexName: 'hello', }), ], - render(h) { - return h(InstantSearchSsr, {}, [ + render: h => + /** + * This code triggers this warning in Vue 3: + * > Non-function value encountered for default slot. Prefer function slots for better performance. + * + * To fix it, replace the third argument + * > [h(...), h(...)] + * with + * > { default: () => [h(...), h(...)] } + * + * but it's not important (and not compatible in vue2), we're leaving it as-is. + */ + h(InstantSearchSsr, {}, [ h(Configure, { attrs: { hitsPerPage: 100, }, }), h(SearchBox), - ]); - }, - // in test, beforeCreated doesn't have $data yet, but IRL it does - created() { - this.instantsearch.hydrate({ - __identifier: 'stringified', - hello: serialized, - }); + ]), + serverPrefetch() { + return this.instantsearch.findResultsState(this); }, }; - const { - vm: { instantsearch }, - } = mount(app); + const wrapper = new Vue({ + mixins: [forceIsServerMixin], + render: h => h(app), + }); - expect(instantsearch.__initialSearchResults).toEqual( - expect.objectContaining({ hello: expect.any(SearchResults) }) - ); + await renderToString(wrapper); - expect(instantsearch.__initialSearchResults.hello).toEqual( - expect.objectContaining(serialized) - ); + expect(searchClient.search).toHaveBeenCalledTimes(1); + expect(searchClient.search.mock.calls[0][0]).toMatchInlineSnapshot(` +Array [ + Object { + "indexName": "hello", + "params": Object { + "facets": Array [], + "hitsPerPage": 100, + "query": "", + "tagFilters": "", + }, + }, +] +`); }); + }); - it('accepts non-stringified results', () => { + describe('hydrate', () => { + it('sets _initialResults', () => { const serialized = createSerializedState(); - const nonSerialized = new SearchResults( - new SearchParameters(serialized._state), - serialized._rawResults - ); - const app = { + let instantsearch; + const app = new Vue({ mixins: [ createServerRootMixin({ searchClient: createFakeClient(), - indexName: 'movies', + indexName: 'hello', }), ], render(h) { @@ -590,27 +598,35 @@ Array [ }, // in test, beforeCreated doesn't have $data yet, but IRL it does created() { + instantsearch = this.instantsearch; this.instantsearch.hydrate({ - movies: nonSerialized, + hello: serialized, }); }, - }; + }); - const { - vm: { instantsearch }, - } = mount(app); + mount(app); - expect(instantsearch.__initialSearchResults).toEqual( - expect.objectContaining({ movies: expect.any(SearchResults) }) + expect(instantsearch._initialResults).toEqual( + expect.objectContaining({ + hello: { + state: expect.any(Object), + results: expect.any(Object), + }, + }) ); - expect(instantsearch.__initialSearchResults.movies).toBe(nonSerialized); + expect(instantsearch._initialResults.hello).toEqual( + expect.objectContaining(serialized) + ); }); it('inits the main index', () => { const serialized = createSerializedState(); - const app = { + let instantsearch; + + const app = new Vue({ mixins: [ createServerRootMixin({ searchClient: createFakeClient(), @@ -627,28 +643,30 @@ Array [ h(SearchBox), ]); }, - }; + created() { + instantsearch = this.instantsearch; + }, + }); - const { - vm: { instantsearch }, - } = mount(app); + mount(app); expect(instantsearch.mainIndex.getHelper()).toBe(null); instantsearch.hydrate({ - __identifier: 'stringified', hello: serialized, }); - // TODO: assert that this is expect.any(AlgoliaSearchHelper), but test fails - // even though it's an object with all the right properties (including constructor) - expect(instantsearch.mainIndex.getHelper()).not.toBeNull(); + expect(instantsearch.mainIndex.getHelper().constructor.name).toBe( + 'AlgoliaSearchHelper' + ); }); it('sets helper & mainHelper', () => { const serialized = createSerializedState(); - const app = { + let instantsearch; + + const app = new Vue({ mixins: [ createServerRootMixin({ searchClient: createFakeClient(), @@ -665,22 +683,24 @@ Array [ h(SearchBox), ]); }, - }; + created() { + instantsearch = this.instantsearch; + }, + }); - const { - vm: { instantsearch }, - } = mount(app); + mount(app); expect(instantsearch.helper).toBe(null); expect(instantsearch.mainHelper).toBe(null); instantsearch.hydrate({ - __identifier: 'stringified', hello: serialized, }); - expect(instantsearch.helper).toEqual(expect.any(AlgoliaSearchHelper)); - expect(instantsearch.mainHelper).toEqual(expect.any(AlgoliaSearchHelper)); + expect(instantsearch.helper.constructor.name).toBe('AlgoliaSearchHelper'); + expect(instantsearch.mainHelper.constructor.name).toBe( + 'AlgoliaSearchHelper' + ); }); it('works when component is at root (and therefore has no $vnode)', async () => { @@ -799,6 +819,7 @@ Array [ results: expect.anything(), }), ]), + parent: expect.anything(), state: expect.anything(), instantSearchInstance: expect.anything(), }, @@ -807,6 +828,7 @@ Object { "createURL": [Function], "helper": Anything, "instantSearchInstance": Anything, + "parent": Anything, "results": Anything, "scopedResults": ArrayContaining [ ObjectContaining { @@ -825,6 +847,58 @@ Object { ); }); + it('uses the results passed to hydrate for rendering', () => { + let instantSearchInstance; + mount({ + mixins: [ + createServerRootMixin({ + searchClient: createFakeClient(), + indexName: 'lol', + }), + ], + created() { + instantSearchInstance = this.instantsearch; + }, + render() {}, + }); + + const widget = { + init: jest.fn(), + render: jest.fn(), + }; + + const resultsState = createSerializedState(); + const state = new SearchParameters(resultsState.state); + const results = new SearchResults(state, resultsState.results); + + instantSearchInstance.hydrate({ + lol: resultsState, + }); + + instantSearchInstance.__forceRender( + widget, + instantSearchInstance.mainIndex + ); + + expect(widget.init).toHaveBeenCalledTimes(0); + expect(widget.render).toHaveBeenCalledTimes(1); + + const renderArgs = widget.render.mock.calls[0][0]; + + expect(renderArgs).toEqual( + expect.objectContaining({ + state, + results, + scopedResults: [ + expect.objectContaining({ + indexId: 'lol', + results, + }), + ], + }) + ); + }); + describe('createURL', () => { it('returns # if instantsearch has no routing', () => { const app = new Vue({ diff --git a/src/util/createServerRootMixin.js b/src/util/createServerRootMixin.js index 2d7054d97..4f11604a9 100644 --- a/src/util/createServerRootMixin.js +++ b/src/util/createServerRootMixin.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import instantsearch from 'instantsearch.js/es'; -import algoliaHelper from 'algoliasearch-helper'; -const { SearchResults, SearchParameters } = algoliaHelper; import { warn } from './warn'; function walkIndex(indexWidget, visit) { @@ -70,18 +68,10 @@ function defaultCloneComponent(componentInstance) { return app; } -function augmentInstantSearch( - instantSearchOptions, - searchClient, - indexName, - cloneComponent -) { - /* eslint-disable no-param-reassign */ - - const helper = algoliaHelper(searchClient, indexName); +function augmentInstantSearch(instantSearchOptions, cloneComponent) { const search = instantsearch(instantSearchOptions); - let resultsState; + let initialResults; /** * main API for SSR, called in serverPrefetch of a root component which contains instantsearch @@ -100,54 +90,38 @@ function augmentInstantSearch( } let app; + let instance; return Promise.resolve() .then(() => { app = cloneComponent(componentInstance); - app.instantsearch.helper = helper; - app.instantsearch.mainHelper = helper; + instance = app.instantsearch; - app.instantsearch.mainIndex.init({ - instantSearchInstance: app.instantsearch, - parent: null, - uiState: app.instantsearch._initialUiState, - }); + instance.start(); + // although we use start for initializing the main index, + // we don't want to send search requests yet + instance.started = false; }) .then(() => renderToString(app, _renderToString)) - .then(() => searchOnlyWithDerivedHelpers(helper)) + .then(() => searchOnlyWithDerivedHelpers(instance.mainHelper)) .then(() => { - const results = {}; - walkIndex(app.instantsearch.mainIndex, widget => { - results[widget.getIndexId()] = widget.getResults(); + initialResults = {}; + walkIndex(instance.mainIndex, widget => { + const { _state, _rawResults } = widget.getResults(); + + initialResults[widget.getIndexId()] = { + // copy just the values of SearchParameters, not the functions + state: Object.keys(_state).reduce((acc, key) => { + // eslint-disable-next-line no-param-reassign + acc[key] = _state[key]; + return acc; + }, {}), + results: _rawResults, + }; }); - search.hydrate(results); - - resultsState = Object.keys(results) - .map(indexId => { - const { _state, _rawResults } = results[indexId]; - return [ - indexId, - { - // copy just the values of SearchParameters, not the functions - _state: Object.keys(_state).reduce((acc, key) => { - acc[key] = _state[key]; - return acc; - }, {}), - _rawResults, - }, - ]; - }) - .reduce( - (acc, [key, val]) => { - acc[key] = val; - return acc; - }, - { - __identifier: 'stringified', - } - ); + search.hydrate(initialResults); return search.getState(); }); }; @@ -156,10 +130,10 @@ function augmentInstantSearch( * @returns {Promise} result state to serialize and enter into .hydrate */ search.getState = function() { - if (!resultsState) { + if (!initialResults) { throw new Error('You need to wait for findResultsState to finish'); } - return resultsState; + return initialResults; }; /** @@ -171,18 +145,17 @@ function augmentInstantSearch( * @returns {void} */ search.__forceRender = function(widget, parent) { - const localHelper = parent.getHelper(); - - const results = search.__initialSearchResults[parent.getIndexId()]; + const results = parent.getResults(); // this happens when a different InstantSearch gets rendered initially, // after the hydrate finished. There's thus no initial results available. - if (!results) { + if (results === null) { return; } const state = results._state; + const localHelper = parent.getHelper(); // helper gets created in init, but that means it doesn't get the injected // parameters, because those are from the lastResults localHelper.state = state; @@ -190,11 +163,8 @@ function augmentInstantSearch( widget.render({ helper: localHelper, results, - scopedResults: parent.getScopedResults().map(result => - Object.assign(result, { - results: search.__initialSearchResults[result.indexId], - }) - ), + scopedResults: parent.getScopedResults(), + parent, state, templatesConfig: {}, createURL: parent.createURL, @@ -218,55 +188,18 @@ function augmentInstantSearch( return; } - const initialResults = - results.__identifier === 'stringified' - ? Object.keys(results).reduce((acc, indexId) => { - if (indexId === '__identifier') { - return acc; - } - acc[indexId] = new SearchResults( - new SearchParameters(results[indexId]._state), - results[indexId]._rawResults - ); - return acc; - }, {}) - : results; - - search.__initialSearchResults = initialResults; + search._initialResults = results; - search.helper = helper; - search.mainHelper = helper; - - search.mainIndex.init({ - instantSearchInstance: search, - parent: null, - uiState: search._initialUiState, - }); + search.start(); + search.started = false; }; - - /* eslint-enable no-param-reassign */ return search; } export function createServerRootMixin(instantSearchOptions = {}) { - const { - searchClient, - indexName, - $cloneComponent = defaultCloneComponent, - } = instantSearchOptions; - - if (!searchClient || !indexName) { - throw new Error( - 'createServerRootMixin requires `searchClient` and `indexName` in the first argument' - ); - } + const { $cloneComponent = defaultCloneComponent } = instantSearchOptions; - const search = augmentInstantSearch( - instantSearchOptions, - searchClient, - indexName, - $cloneComponent - ); + const search = augmentInstantSearch(instantSearchOptions, $cloneComponent); // put this in the user's root Vue instance // we can then reuse that InstantSearch instance seamlessly from `ais-instant-search-ssr` @@ -278,7 +211,7 @@ export function createServerRootMixin(instantSearchOptions = {}) { }, data() { return { - // this is in data, so that the real & duplicated render do not share + // this is in data, so that the real & cloned render do not share // the same instantsearch instance. instantsearch: search, }; diff --git a/src/util/testutils/helper.js b/src/util/testutils/helper.js index 0c8e86da8..b79b82a65 100644 --- a/src/util/testutils/helper.js +++ b/src/util/testutils/helper.js @@ -1,5 +1,5 @@ export const createSerializedState = () => ({ - _rawResults: [ + results: [ { hits: [ { @@ -50,7 +50,7 @@ export const createSerializedState = () => ({ index: 'movies', }, ], - _state: { + state: { index: 'movies', query: 'hi', facets: [], diff --git a/yarn.lock b/yarn.lock index c6603c491..793a41d21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,6 +66,11 @@ "@algolia/requester-common" "4.0.1" "@algolia/transporter" "4.0.1" +"@algolia/events@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" + integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== + "@algolia/logger-common@4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.0.1.tgz#177debee9a30d14f9286c0eb991e81ba61d2479e" @@ -745,6 +750,11 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/qs@^6.5.3": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + "@types/request@^2.48.4": version "2.48.5" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" @@ -1154,19 +1164,19 @@ ajv@^6.5.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -algoliasearch-helper@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.1.0.tgz#f8725bd6f0d1f515955d720ccd617a3af4ac695f" - integrity sha512-d48U2GIsGJr/fVV+W7Z1Ud6GWLSblKQgA71M254YNtxvniKFsbI0Z6hQZ/8yodfGWHjJ4dETeb7ihGKQaXihUw== +algoliasearch-helper@3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.6.2.tgz#45e19b12589cfa0c611b573287f65266ea2cc14a" + integrity sha512-Xx0NOA6k4ySn+R2l3UMSONAaMkyfmrZ3AP1geEMo32MxDJQJesZABZYsldO9fa6FKQxH91afhi4hO1G0Zc2opg== dependencies: events "^1.1.1" -algoliasearch-helper@^3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.5.4.tgz#21b20ab8a258daa9dde9aef2daa5e8994cd66077" - integrity sha512-t+FLhXYnPZiwjYe5ExyN962HQY8mi3KwRju3Lyf6OBgtRdx30d6mqvtClXf5NeBihH45Xzj6t4Y5YyvAI432XA== +algoliasearch-helper@^3.6.2: + version "3.7.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.7.0.tgz#c0a0493df84d850360f664ad7a9d4fc78a94fd78" + integrity sha512-XJ3QfERBLfeVCyTVx80gon7r3/rgm/CE8Ha1H7cbablRe/X7SfYQ14g/eO+MhjVKIQp+gy9oC6G5ilmLwS1k6w== dependencies: - events "^1.1.1" + "@algolia/events" "^4.0.1" algoliasearch@4.0.1: version "4.0.1" @@ -7897,19 +7907,21 @@ instantsearch.css@7.3.1: resolved "https://registry.yarnpkg.com/instantsearch.css/-/instantsearch.css-7.3.1.tgz#7ab74a8f355091ae040947a9cf5438f379026622" integrity sha512-/kaMDna5D+Q9mImNBHEhb9HgHATDOFKYii7N1Iwvrj+lmD9gBJLqVhUw67gftq2O0QI330pFza+CRscIwB1wQQ== -instantsearch.js@^4.25.0: - version "4.25.0" - resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.25.0.tgz#0c631479afa0826af0adee120dc2bbb68e1caeb3" - integrity sha512-146u/zlu+lHjMgykKnvYr5yvM4xB/CHN8jmf5872gYNfjoZlmQ6GAoYwrgRjQrbLAbIZ6hhpXkDhwmoptmvUgQ== +instantsearch.js@^4.34.0: + version "4.34.0" + resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.34.0.tgz#f9fda9354bea206ddfbb22891a93c0b228f582be" + integrity sha512-qc4boCWDMXYMKbi2rvHJZPXKvGS6XNxrEBZk10QGc9WF9BeARVWB4KUBcseuAitPyFpW9DDOUJ2RtFuDlrsM0A== dependencies: "@types/google.maps" "^3.45.3" "@types/hogan.js" "^3.0.0" - algoliasearch-helper "^3.5.4" + "@types/qs" "^6.5.3" + algoliasearch-helper "^3.6.2" classnames "^2.2.5" events "^1.1.0" hogan.js "^3.0.2" preact "^10.0.0" - qs "^6.5.1" + qs "^6.5.1 < 6.10" + search-insights "^2.1.0" interpret@^1.0.0: version "1.0.4" @@ -12136,6 +12148,11 @@ qs@^6.5.1, qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +"qs@^6.5.1 < 6.10": + version "6.9.7" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" + integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== + qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -13560,6 +13577,11 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" +search-insights@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.1.0.tgz#42822fa325062ec5bf050c5f8cad58f0c5b21310" + integrity sha512-mWIVsZ5igQnlM2tC0HJCtGWKQcBhLhkasZoZZS2exUfZPpRowVWEvyyYTWU3M0rUKWlWLWFCYvJ3HGTSSCsOcw== + seek-bzip@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc"