Added improved Admin search behind labs flag (#20363)

ref https://linear.app/tryghost/issue/MOM-117
ref https://linear.app/tryghost/issue/MOM-70

- moved current search into new `search-provider` service and updated `search` service to use the provider service internally
- added `search-provider-beta` service
  - uses `flexsearch` as the underlying index for each document so we have better indexing and matching compared to the naive exact-match search we had previously
  - adds `excerpt` matching for posts and pages
  - keeps results output the same as the original search provider
- added `internalLinkingSearchImprovements` labs flag so we can test this internally before reaching our internal linking beta testers
- updated `search` service to switch between providers based on labs flag
This commit is contained in:
Kevin Ansfield 2024-06-11 16:18:28 +01:00 committed by GitHub
parent bea074b23d
commit 54812dc67a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 532 additions and 362 deletions

View File

@ -63,6 +63,10 @@ const features = [{
title: 'Internal Linking @-links (internal alpha)',
description: 'Adds internal URL search when typing @ in the editor',
flag: 'internalLinkingAtLinks'
},{
title: 'Internal Linking search improvements (internal alpha)',
description: 'Replaces Admin\'s search with flexsearch indexes',
flag: 'internalLinkingSearchImprovements'
},{
title: 'ActivityPub',
description: '(Highly) Experimental support for ActivityPub.',

View File

@ -84,6 +84,7 @@ export default class FeatureService extends Service {
@feature('ActivityPub') ActivityPub;
@feature('internalLinking') internalLinking;
@feature('internalLinkingAtLinks') internalLinkingAtLinks;
@feature('internalLinkingSearchImprovements') internalLinkingSearchImprovements;
@feature('editorExcerpt') editorExcerpt;
@feature('newsletterExcerpt') newsletterExcerpt;

View File

@ -0,0 +1,136 @@
import RSVP from 'rsvp';
import Service from '@ember/service';
import {default as Flexsearch} from 'flexsearch';
import {isEmpty} from '@ember/utils';
import {pluralize} from 'ember-inflector';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
const {Document} = Flexsearch;
export const SEARCHABLES = [
{
name: 'Staff',
model: 'user',
fields: ['id', 'slug', 'url', 'name', 'profile_image'],
pathField: 'slug',
titleField: 'name',
index: ['name']
},
{
name: 'Tags',
model: 'tag',
fields: ['id', 'slug', 'url', 'name'],
pathField: 'slug',
titleField: 'name',
index: ['name']
},
{
name: 'Posts',
model: 'post',
fields: ['id', 'url', 'title', 'excerpt', 'status', 'published_at', 'visibility'],
pathField: 'id',
titleField: 'title',
index: ['title', 'excerpt']
},
{
name: 'Pages',
model: 'page',
fields: ['id', 'url', 'title', 'excerpt', 'status', 'published_at', 'visibility'],
pathField: 'id',
titleField: 'title',
index: ['title', 'excerpt']
}
];
export default class SearchProviderService extends Service {
@service ajax;
@service notifications;
@service store;
indexes = SEARCHABLES.reduce((indexes, searchable) => {
indexes[searchable.model] = new Document({
tokenize: 'forward',
document: {
id: 'id',
index: searchable.index,
store: true
}
});
return indexes;
}, {});
/* eslint-disable require-yield */
@task
*searchTask(term) {
const results = [];
SEARCHABLES.forEach((searchable) => {
const searchResults = this.indexes[searchable.model].search(term, {enrich: true});
const usedIds = new Set();
const groupResults = [];
searchResults.forEach((field) => {
field.result.forEach((searchResult) => {
const {id, doc} = searchResult;
if (usedIds.has(id)) {
return;
}
usedIds.add(id);
groupResults.push({
id: `${searchable.model}.${doc[searchable.pathField]}`,
title: doc[searchable.titleField],
groupName: searchable.name,
status: doc.status,
visibility: doc.visibility,
publishedAt: doc.published_at
});
});
});
if (!isEmpty(groupResults)) {
results.push({
groupName: searchable.name,
options: groupResults
});
}
});
return results;
}
/* eslint-enable require-yield */
@task
*refreshContentTask() {
try {
const promises = SEARCHABLES.map(searchable => this.#loadSearchable(searchable));
yield RSVP.all(promises);
} catch (error) {
// eslint-disable-next-line
console.error(error);
}
}
async #loadSearchable(searchable) {
const url = `${this.store.adapterFor(searchable.model).urlForQuery({}, searchable.model)}/`;
const query = {fields: searchable.fields, limit: 10000};
try {
const response = await this.ajax.request(url, {data: query});
response[pluralize(searchable.model)].forEach((item) => {
this.indexes[searchable.model].add(item);
});
} catch (error) {
console.error(error); // eslint-disable-line
this.notifications.showAPIError(error, {
key: `search.load${searchable.name}.error`
});
}
}
}

View File

@ -0,0 +1,116 @@
import RSVP from 'rsvp';
import Service from '@ember/service';
import {isEmpty} from '@ember/utils';
import {pluralize} from 'ember-inflector';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export const SEARCHABLES = [
{
name: 'Staff',
model: 'user',
fields: ['id', 'slug', 'url', 'name'], // id not used but required for API to have correct url
idField: 'slug',
titleField: 'name'
},
{
name: 'Tags',
model: 'tag',
fields: ['slug', 'url', 'name'],
idField: 'slug',
titleField: 'name'
},
{
name: 'Posts',
model: 'post',
fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'],
idField: 'id',
titleField: 'title'
},
{
name: 'Pages',
model: 'page',
fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'],
idField: 'id',
titleField: 'title'
}
];
export default class SearchProviderService extends Service {
@service ajax;
@service notifications;
@service store;
content = [];
/* eslint-disable require-yield */
@task
*searchTask(term) {
const normalizedTerm = term.toString().toLowerCase();
const results = [];
SEARCHABLES.forEach((searchable) => {
const matchedContent = this.content.filter((item) => {
const normalizedTitle = item.title.toString().toLowerCase();
return (
item.groupName === searchable.name &&
normalizedTitle.indexOf(normalizedTerm) >= 0
);
});
if (!isEmpty(matchedContent)) {
results.push({
groupName: searchable.name,
options: matchedContent
});
}
});
return results;
}
/* eslint-enable require-yield */
@task
*refreshContentTask() {
const content = [];
const promises = SEARCHABLES.map(searchable => this._loadSearchable(searchable, content));
try {
yield RSVP.all(promises);
this.content = content;
} catch (error) {
// eslint-disable-next-line
console.error(error);
}
}
async _loadSearchable(searchable, content) {
const url = `${this.store.adapterFor(searchable.model).urlForQuery({}, searchable.model)}/`;
const maxSearchableLimit = '10000';
const query = {fields: searchable.fields, limit: maxSearchableLimit};
try {
const response = await this.ajax.request(url, {data: query});
const items = response[pluralize(searchable.model)].map(
item => ({
id: `${searchable.model}.${item[searchable.idField]}`,
url: item.url,
title: item[searchable.titleField],
groupName: searchable.name,
status: item.status,
visibility: item.visibility,
publishedAt: item.published_at
})
);
content.push(...items);
} catch (error) {
console.error(error); // eslint-disable-line
this.notifications.showAPIError(error, {
key: `search.load${searchable.name}.error`
});
}
}
}

View File

@ -1,49 +1,24 @@
import RSVP from 'rsvp';
import Service from '@ember/service';
import {action} from '@ember/object';
import {isBlank, isEmpty} from '@ember/utils';
import {pluralize} from 'ember-inflector';
import {isBlank} from '@ember/utils';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
export default class SearchService extends Service {
@service ajax;
@service feature;
@service notifications;
@service searchProvider;
@service searchProviderBeta;
@service store;
content = [];
isContentStale = true;
searchables = [
{
name: 'Staff',
model: 'user',
fields: ['id', 'slug', 'url', 'name'], // id not used but required for API to have correct url
idField: 'slug',
titleField: 'name'
},
{
name: 'Tags',
model: 'tag',
fields: ['slug', 'url', 'name'],
idField: 'slug',
titleField: 'name'
},
{
name: 'Posts',
model: 'post',
fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'],
idField: 'id',
titleField: 'title'
},
{
name: 'Pages',
model: 'page',
fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'],
idField: 'id',
titleField: 'title'
}
];
get provider() {
return this.feature.internalLinkingSearchImprovements
? this.searchProviderBeta
: this.searchProvider;
}
@action
expireContent() {
@ -67,33 +42,7 @@ export default class SearchService extends Service {
yield this.refreshContentTask.lastRunning;
}
const searchResult = this._searchContent(term);
return searchResult;
}
_searchContent(term) {
const normalizedTerm = term.toString().toLowerCase();
const results = [];
this.searchables.forEach((searchable) => {
const matchedContent = this.content.filter((item) => {
const normalizedTitle = item.title.toString().toLowerCase();
return (
item.groupName === searchable.name &&
normalizedTitle.indexOf(normalizedTerm) >= 0
);
});
if (!isEmpty(matchedContent)) {
results.push({
groupName: searchable.name,
options: matchedContent
});
}
});
return results;
return yield this.provider.searchTask.perform(term);
}
@task({drop: true})
@ -104,47 +53,8 @@ export default class SearchService extends Service {
this.isContentStale = true;
const content = [];
const promises = this.searchables.map(searchable => this._loadSearchable(searchable, content));
try {
yield RSVP.all(promises);
this.content = content;
} catch (error) {
// eslint-disable-next-line
console.error(error);
}
yield this.provider.refreshContentTask.perform();
this.isContentStale = false;
}
async _loadSearchable(searchable, content) {
const url = `${this.store.adapterFor(searchable.model).urlForQuery({}, searchable.model)}/`;
const maxSearchableLimit = '10000';
const query = {fields: searchable.fields, limit: maxSearchableLimit};
try {
const response = await this.ajax.request(url, {data: query});
const items = response[pluralize(searchable.model)].map(
item => ({
id: `${searchable.model}.${item[searchable.idField]}`,
url: item.url,
title: item[searchable.titleField],
groupName: searchable.name,
status: item.status,
visibility: item.visibility,
publishedAt: item.published_at
})
);
content.push(...items);
} catch (error) {
console.error(error); // eslint-disable-line
this.notifications.showAPIError(error, {
key: `search.load${searchable.name}.error`
});
}
}
}

View File

@ -123,6 +123,7 @@
"ember-websockets": "10.2.1",
"eslint-plugin-babel": "5.3.1",
"faker": "5.5.3",
"flexsearch": "0.7.43",
"fs-extra": "11.2.0",
"glob": "8.1.0",
"google-caja-bower": "https://github.com/acburdine/google-caja-bower#ghost",
@ -205,4 +206,4 @@
}
}
}
}
}

View File

@ -2,167 +2,184 @@ import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import {authenticateSession} from 'ember-simple-auth/test-support';
import {click, currentURL, find, findAll, triggerKeyEvent, visit} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {enableLabsFlag} from '../helpers/labs-flag';
import {expect} from 'chai';
import {getPosts} from '../../mirage/config/posts';
import {setupApplicationTest} from 'ember-mocha';
import {setupMirage} from 'ember-cli-mirage/test-support';
import {typeInSearch} from 'ember-power-select/test-support/helpers';
describe('Acceptance: Search', function () {
const trigger = '[data-test-modal="search"] .ember-power-select-trigger';
// eslint-disable-next-line no-unused-vars
let firstUser, firstPost, secondPost, firstPage, firstTag;
const suites = [{
name: 'Acceptance: Search',
beforeEach() {
// noop
}
}, {
name: 'Acceptance: Search (beta)',
beforeEach() {
enableLabsFlag(this.server, 'internalLinking');
}
}];
const hooks = setupApplicationTest();
setupMirage(hooks);
suites.forEach((suite) => {
describe(suite.name, function () {
const trigger = '[data-test-modal="search"] .ember-power-select-trigger';
// eslint-disable-next-line no-unused-vars
let firstUser, firstPost, secondPost, firstPage, firstTag;
this.beforeEach(async function () {
this.server.loadFixtures();
const hooks = setupApplicationTest();
setupMirage(hooks);
// create user to authenticate as
let role = this.server.create('role', {name: 'Owner'});
firstUser = this.server.create('user', {roles: [role], slug: 'owner', name: 'First user'});
this.beforeEach(async function () {
this.server.loadFixtures();
// populate store with data we'll be searching
firstPost = this.server.create('post', {title: 'First post', slug: 'first-post'});
secondPost = this.server.create('post', {title: 'Second post', slug: 'second-post'});
firstPage = this.server.create('page', {title: 'First page', slug: 'first-page'});
firstTag = this.server.create('tag', {name: 'First tag', slug: 'first-tag'});
// create user to authenticate as
let role = this.server.create('role', {name: 'Owner'});
firstUser = this.server.create('user', {roles: [role], slug: 'owner', name: 'First user'});
return await authenticateSession();
});
// populate store with data we'll be searching
firstPost = this.server.create('post', {title: 'First post', slug: 'first-post'});
secondPost = this.server.create('post', {title: 'Second post', slug: 'second-post'});
firstPage = this.server.create('page', {title: 'First page', slug: 'first-page'});
firstTag = this.server.create('tag', {name: 'First tag', slug: 'first-tag'});
it('opens search modal when clicking icon', async function () {
await visit('/dashboard');
expect(currentURL(), 'currentURL').to.equal('/dashboard');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
await click('[data-test-button="search"]');
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
});
suite.beforeEach.bind(this)();
it('opens search icon when pressing Ctrl/Cmd+K', async function () {
await visit('/dashboard');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
await triggerKeyEvent(document, 'keydown', 'K', {
metaKey: ctrlOrCmd === 'command',
ctrlKey: ctrlOrCmd === 'ctrl'
return await authenticateSession();
});
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
});
it('closes search modal on escape key', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
await triggerKeyEvent(document, 'keydown', 'Escape');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
});
it('opens search modal when clicking icon', async function () {
await visit('/dashboard');
expect(currentURL(), 'currentURL').to.equal('/dashboard');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
await click('[data-test-button="search"]');
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
});
it('closes search modal on click outside', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
await click('.epm-backdrop');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
});
it('opens search icon when pressing Ctrl/Cmd+K', async function () {
await visit('/dashboard');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
await triggerKeyEvent(document, 'keydown', 'K', {
metaKey: ctrlOrCmd === 'command',
ctrlKey: ctrlOrCmd === 'ctrl'
});
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
});
it('finds posts, pages, staff, and tags when typing', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first'); // search is not case sensitive
it('closes search modal on escape key', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
await triggerKeyEvent(document, 'keydown', 'Escape');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
});
// all groups are present
const groupNames = findAll('.ember-power-select-group-name');
expect(groupNames, 'group names').to.have.length(4);
expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']);
it('closes search modal on click outside', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
expect(find('[data-test-modal="search"]'), 'search modal').to.exist;
await click('.epm-backdrop');
expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist;
});
// correct results are found
const options = findAll('.ember-power-select-option');
expect(options, 'number of search results').to.have.length(4);
expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']);
it('finds posts, pages, staff, and tags when typing', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first'); // search is not case sensitive
// first item is selected
expect(options[0]).to.have.attribute('aria-current', 'true');
});
// all groups are present
const groupNames = findAll('.ember-power-select-group-name');
expect(groupNames, 'group names').to.have.length(4);
expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']);
it('up/down arrows move selected item', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first post');
expect(findAll('.ember-power-select-option')[0], 'first option (initial)').to.have.attribute('aria-current', 'true');
await triggerKeyEvent(trigger, 'keyup', 'ArrowDown');
expect(findAll('.ember-power-select-option')[0], 'second option (after down)').to.have.attribute('aria-current', 'true');
await triggerKeyEvent(trigger, 'keyup', 'ArrowUp');
expect(findAll('.ember-power-select-option')[0], 'first option (after up)').to.have.attribute('aria-current', 'true');
});
// correct results are found
const options = findAll('.ember-power-select-option');
expect(options, 'number of search results').to.have.length(4);
expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']);
it('navigates to editor when post selected (Enter)', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first post');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`);
});
// first item is selected
expect(options[0]).to.have.attribute('aria-current', 'true');
});
it('navigates to editor when post selected (Clicked)', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first post');
await click('.ember-power-select-option[aria-current="true"]');
expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`);
});
it('up/down arrows move selected item', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first post');
expect(findAll('.ember-power-select-option')[0], 'first option (initial)').to.have.attribute('aria-current', 'true');
await triggerKeyEvent(trigger, 'keyup', 'ArrowDown');
expect(findAll('.ember-power-select-option')[0], 'second option (after down)').to.have.attribute('aria-current', 'true');
await triggerKeyEvent(trigger, 'keyup', 'ArrowUp');
expect(findAll('.ember-power-select-option')[0], 'first option (after up)').to.have.attribute('aria-current', 'true');
});
it('navigates to editor when page selected', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('page');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting page').to.equal(`/editor/page/${firstPage.id}`);
});
it('navigates to editor when post selected (Enter)', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first post');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`);
});
it('navigates to tag edit screen when tag selected', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('tag');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting tag').to.equal(`/tags/${firstTag.slug}`);
});
it('navigates to editor when post selected (Clicked)', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first post');
await click('.ember-power-select-option[aria-current="true"]');
expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`);
});
// TODO: Staff settings are now part of AdminX so this isn't working, can we test AdminX from Ember tests?
it.skip('navigates to user edit screen when user selected', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('user');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting user').to.equal(`/settings/staff/${firstUser.slug}`);
});
it('navigates to editor when page selected', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('page');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting page').to.equal(`/editor/page/${firstPage.id}`);
});
it('shows no results message when no results', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('x');
expect(find('.ember-power-select-option--no-matches-message'), 'no results message').to.contain.text('No results found');
});
it('navigates to tag edit screen when tag selected', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('tag');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting tag').to.equal(`/tags/${firstTag.slug}`);
});
// https://linear.app/tryghost/issue/MOM-103/search-stalls-on-query-when-refresh-occurs
it('handles refresh on first search being slow', async function () {
this.server.get('/posts/', getPosts, {timing: 200});
// TODO: Staff settings are now part of AdminX so this isn't working, can we test AdminX from Ember tests?
it.skip('navigates to user edit screen when user selected', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('user');
await triggerKeyEvent(trigger, 'keydown', 'Enter');
expect(currentURL(), 'url after selecting user').to.equal(`/settings/staff/${firstUser.slug}`);
});
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first'); // search is not case sensitive
it('shows no results message when no results', async function () {
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('x');
expect(find('.ember-power-select-option--no-matches-message'), 'no results message').to.contain.text('No results found');
});
// all groups are present
const groupNames = findAll('.ember-power-select-group-name');
expect(groupNames, 'group names').to.have.length(4);
expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']);
// https://linear.app/tryghost/issue/MOM-103/search-stalls-on-query-when-refresh-occurs
it('handles refresh on first search being slow', async function () {
this.server.get('/posts/', getPosts, {timing: 200});
// correct results are found
const options = findAll('.ember-power-select-option');
expect(options, 'number of search results').to.have.length(4);
expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']);
await visit('/dashboard');
await click('[data-test-button="search"]');
await typeInSearch('first'); // search is not case sensitive
// first item is selected
expect(options[0]).to.have.attribute('aria-current', 'true');
// all groups are present
const groupNames = findAll('.ember-power-select-group-name');
expect(groupNames, 'group names').to.have.length(4);
expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']);
// correct results are found
const options = findAll('.ember-power-select-option');
expect(options, 'number of search results').to.have.length(4);
expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']);
// first item is selected
expect(options[0]).to.have.attribute('aria-current', 'true');
});
});
});

View File

@ -54,7 +54,8 @@ const ALPHA_FEATURES = [
'importMemberTier',
'lexicalIndicators',
'adminXDemo',
'internalLinkingAtLinks'
'internalLinkingAtLinks',
'internalLinkingSearchImprovements'
];
module.exports.GA_KEYS = [...GA_FEATURES];

242
yarn.lock
View File

@ -2430,7 +2430,7 @@
ember-cli-babel "^7.22.1"
ember-compatibility-helpers "^1.1.1"
"@ember/render-modifiers@2.1.0", "@ember/render-modifiers@^1.0.2 || ^2.0.0", "@ember/render-modifiers@^2.0.0", "@ember/render-modifiers@^2.0.4":
"@ember/render-modifiers@2.1.0", "@ember/render-modifiers@^1.0.2 || ^2.0.0", "@ember/render-modifiers@^2.0.4":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@ember/render-modifiers/-/render-modifiers-2.1.0.tgz#f4fff95a8b5cfbe947ec46644732d511711c5bf9"
integrity sha512-LruhfoDv2itpk0fA0IC76Sxjcnq/7BC6txpQo40hOko8Dn6OxwQfxkPIbZGV0Cz7df+iX+VJrcYzNIvlc3w2EQ==
@ -2568,19 +2568,6 @@
resolve "^1.8.1"
semver "^7.3.2"
"@embroider/macros@0.47.2", "@embroider/macros@^0.47.2":
version "0.47.2"
resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-0.47.2.tgz#23cbe92cac3c24747f054e1eea2a22538bf7ebd0"
integrity sha512-ViNWluJCeM5OPlM3rs8kdOz3RV5rpfXX5D2rDnc/q86xRS0xf4NFEjYRV7W6fBcD0b3v5jSHDTwrjq9Kee4rHg==
dependencies:
"@embroider/shared-internals" "0.47.2"
assert-never "^1.2.1"
ember-cli-babel "^7.26.6"
find-up "^5.0.0"
lodash "^4.17.21"
resolve "^1.20.0"
semver "^7.3.2"
"@embroider/macros@1.13.4", "@embroider/macros@^0.50.0 || ^1.0.0", "@embroider/macros@^1.0.0", "@embroider/macros@^1.10.0", "@embroider/macros@^1.12.2", "@embroider/macros@^1.2.0", "@embroider/macros@^1.8.0", "@embroider/macros@^1.8.3", "@embroider/macros@^1.9.0":
version "1.13.4"
resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-1.13.4.tgz#4fefb79d68bcfbc4619551572b2ca3040a224fb5"
@ -2608,19 +2595,6 @@
semver "^7.3.2"
typescript-memoize "^1.0.0-alpha.3"
"@embroider/shared-internals@0.47.2":
version "0.47.2"
resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-0.47.2.tgz#24e9fa0dd9c529d5c996ee1325729ea08d1fa19f"
integrity sha512-SxdZYjAE0fiM5zGDz+12euWIsQZ1tsfR1k+NKmiWMyLhA5T3pNgbR2/Djvx/cVIxOtEavGGSllYbzRKBtV4xMg==
dependencies:
babel-import-util "^0.2.0"
ember-rfc176-data "^0.3.17"
fs-extra "^9.1.0"
lodash "^4.17.21"
resolve-package-path "^4.0.1"
semver "^7.3.5"
typescript-memoize "^1.0.1"
"@embroider/shared-internals@2.5.1", "@embroider/shared-internals@^2.0.0":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-2.5.1.tgz#a4d8c057cbff293ef6eb29ee6537f263d206b444"
@ -2681,15 +2655,6 @@
broccoli-funnel "^3.0.5"
ember-cli-babel "^7.23.1"
"@embroider/util@^0.47.2":
version "0.47.2"
resolved "https://registry.yarnpkg.com/@embroider/util/-/util-0.47.2.tgz#d06497b4b84c07ed9c7b628293bb019c533f4556"
integrity sha512-g9OqnFJPktGu9NS0Ug3Pxz1JE3jeDceeVE4IrlxDrVmBXMA/GrBvpwjolWgl6jh97cMJyExdz62jIvPHV4256Q==
dependencies:
"@embroider/macros" "0.47.2"
broccoli-funnel "^3.0.5"
ember-cli-babel "^7.23.1"
"@emotion/babel-plugin@^11.11.0":
version "11.11.0"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"
@ -3347,6 +3312,18 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
dependencies:
string-width "^5.1.2"
string-width-cjs "npm:string-width@^4.2.0"
strip-ansi "^7.0.1"
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
"@isaacs/ttlcache@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2"
@ -6952,7 +6929,7 @@
"@tryghost/root-utils" "^0.3.28"
debug "^4.3.1"
"@tryghost/elasticsearch@^3.0.19":
"@tryghost/elasticsearch@^3.0.16", "@tryghost/elasticsearch@^3.0.19":
version "3.0.19"
resolved "https://registry.yarnpkg.com/@tryghost/elasticsearch/-/elasticsearch-3.0.19.tgz#a0a94b667c83575a57775027aea5cb4ff4f216ef"
integrity sha512-7yOPEnkebsMuHeIH5oYRQoHa1vz1AkjHRPN4GWJhcywrH20S3Kj66oUPj+8jyqDOvfwqPc7Vfa6jc7rTcy3AQQ==
@ -6982,7 +6959,15 @@
focus-trap "^6.7.2"
postcss-preset-env "^7.3.1"
"@tryghost/errors@1.3.1", "@tryghost/errors@1.3.2", "@tryghost/errors@^1.2.26", "@tryghost/errors@^1.2.3", "@tryghost/errors@^1.3.2":
"@tryghost/errors@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-1.3.1.tgz#32a00c5e5293c46e54d03a66da871ac34b2ab35c"
integrity sha512-iZqT0vZ3NVZNq9o1HYxW00k1mcUAC+t5OLiI8O29/uQwAfy7NemY+Cabl9mWoIwgvBmw7l0Z8pHTcXMo1c+xMw==
dependencies:
"@stdlib/utils-copy" "^0.0.7"
uuid "^9.0.0"
"@tryghost/errors@1.3.2", "@tryghost/errors@^1.2.26", "@tryghost/errors@^1.2.3", "@tryghost/errors@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-1.3.2.tgz#3612f6f59ca07e37d1095f9eb31ff6a069a3b7f2"
integrity sha512-Aqi2vz7HHwN6p2juIYUu8vpMtaKavjULBKNnL0l1req9qXjPs90i/HV8zhvK0ceeWuPdEXaCkfHSRr/yxG3/uw==
@ -7020,7 +7005,7 @@
resolved "https://registry.yarnpkg.com/@tryghost/http-cache-utils/-/http-cache-utils-0.1.15.tgz#721ffd1f3b7173da679f6c8c25cda6f0c728264b"
integrity sha512-G0x9ZUkBbTWIFg2Dnng6qKLststZKuUYx98pcowNwpYuEGUPOka4eAJj3frz/60F1CzMb1qYy9WEjD/xRGd5oQ==
"@tryghost/http-stream@^0.1.30":
"@tryghost/http-stream@^0.1.27", "@tryghost/http-stream@^0.1.30":
version "0.1.30"
resolved "https://registry.yarnpkg.com/@tryghost/http-stream/-/http-stream-0.1.30.tgz#ef2a9a55a2ec7fab7e889a11ce23e7ebfaab8a0b"
integrity sha512-ZlcwtWQICN2OeHcVAL+BXxkzO5tICdLYZeQ2EuVk2E+mC+1ARFrpVUbpTnIbCXDL+uV3cOLqPh2KNGGJAdDChw==
@ -7206,7 +7191,24 @@
lodash "^4.17.21"
luxon "^1.26.0"
"@tryghost/logging@2.4.10", "@tryghost/logging@2.4.15", "@tryghost/logging@^2.4.7":
"@tryghost/logging@2.4.10":
version "2.4.10"
resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.4.10.tgz#2e5b56c53364be330c1e6f2ffa33e3c30b7bac8e"
integrity sha512-l356vLSQmszY14y7ef5YxY4CZ3418NXn5+LvFdlweeTRk0ilWx1mVUoXi8IlVh90rIVbemv+pXi1dusJB6peQA==
dependencies:
"@tryghost/bunyan-rotating-filestream" "^0.0.7"
"@tryghost/elasticsearch" "^3.0.16"
"@tryghost/http-stream" "^0.1.27"
"@tryghost/pretty-stream" "^0.1.21"
"@tryghost/root-utils" "^0.3.25"
bunyan "^1.8.15"
bunyan-loggly "^1.4.2"
fs-extra "^11.0.0"
gelf-stream "^1.1.1"
json-stringify-safe "^5.0.1"
lodash "^4.17.21"
"@tryghost/logging@2.4.15", "@tryghost/logging@^2.4.7":
version "2.4.15"
resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.4.15.tgz#a94e37d760a62d6f2fc2868e4cd8bf6f219b2a2e"
integrity sha512-mSVdSR/9bd1D/DCFpfeFn2AnPE/0lK78ePHBrtteOipA7ogL0Kd+QvabHK5iKLe+/20flBZs4BvnU/DBuS8Pvw==
@ -7295,7 +7297,7 @@
chalk "^4.1.0"
sywac "^1.3.0"
"@tryghost/pretty-stream@^0.1.24":
"@tryghost/pretty-stream@^0.1.21", "@tryghost/pretty-stream@^0.1.24":
version "0.1.24"
resolved "https://registry.yarnpkg.com/@tryghost/pretty-stream/-/pretty-stream-0.1.24.tgz#f670dd66d0d3f7fa733f72294b42fc1b40b0fbd3"
integrity sha512-s83KJXIt2nN4JRtBkkWWPLgmh8d/FKd8pvlln4L+EOKkNiVie2vEeATphFsWiK2NZOc4GSgv/fZHxoKBvnOrnA==
@ -7326,7 +7328,7 @@
got "13.0.0"
lodash "^4.17.21"
"@tryghost/root-utils@0.3.28", "@tryghost/root-utils@^0.3.24", "@tryghost/root-utils@^0.3.28":
"@tryghost/root-utils@0.3.28", "@tryghost/root-utils@^0.3.24", "@tryghost/root-utils@^0.3.25", "@tryghost/root-utils@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@tryghost/root-utils/-/root-utils-0.3.28.tgz#43ae0047927a7753c9b526ea12ce6e382ec7fb1f"
integrity sha512-/izwMw9tCJIQ3DVHumzEWgKhKAw5FwTgrrYcCNHl89yijJKaVRBOJUhlB/u2ST6UWfhahodjaYauq7ymTItaeg==
@ -8940,7 +8942,7 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
ansi-styles@^6.0.0, ansi-styles@^6.2.1:
ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
@ -13976,6 +13978,11 @@ duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@ -14161,7 +14168,7 @@ ember-auto-import@2.7.3, "ember-auto-import@^1.12.1 || ^2.4.3", ember-auto-impor
typescript-memoize "^1.0.0-alpha.3"
walk-sync "^3.0.0"
ember-auto-import@^1.11.2, ember-auto-import@^1.11.3, ember-auto-import@^1.12.0:
ember-auto-import@^1.11.3, ember-auto-import@^1.12.0:
version "1.12.2"
resolved "https://registry.yarnpkg.com/ember-auto-import/-/ember-auto-import-1.12.2.tgz#cc7298ee5c0654b0249267de68fb27a2861c3579"
integrity sha512-gLqML2k77AuUiXxWNon1FSzuG1DV7PEPpCLCU5aJvf6fdL6rmFfElsZRh+8ELEB/qP9dT+LHjNEunVzd2dYc8A==
@ -14196,25 +14203,7 @@ ember-auto-import@^1.11.2, ember-auto-import@^1.11.3, ember-auto-import@^1.12.0:
walk-sync "^0.3.3"
webpack "^4.43.0"
ember-basic-dropdown@^3.0.11:
version "3.1.0"
resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-3.1.0.tgz#47c292de890d1958057736c00b8eb2b8017d530b"
integrity sha512-UISvgJHfiJ8FeXqH8ZN+NmoImN8p6Sb+85qlEv853hLuEfEYnFUqLNhea8nNl9CpFqcD3yU4dKbhYtc6nB39aQ==
dependencies:
"@ember/render-modifiers" "^2.0.0"
"@embroider/macros" "^0.47.2"
"@embroider/util" "^0.47.2"
"@glimmer/component" "^1.0.4"
"@glimmer/tracking" "^1.0.4"
ember-cli-babel "^7.26.6"
ember-cli-htmlbars "^6.0.0"
ember-cli-typescript "^4.2.1"
ember-element-helper "^0.5.5"
ember-maybe-in-element "^2.0.3"
ember-style-modifier "^0.7.0"
ember-truth-helpers "^2.1.0 || ^3.0.0"
ember-basic-dropdown@^6.0.0:
ember-basic-dropdown@6.0.2, ember-basic-dropdown@^3.0.11, ember-basic-dropdown@^6.0.0:
version "6.0.2"
resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-6.0.2.tgz#af47dbd544c605cf9cbc62225185616356aeef52"
integrity sha512-JgI/cy7eS/Y2WoQl7B2Mko/1aFTAlxr5d+KpQeH7rBKOFml7IQtLvhiDQrpU/FLkrQ9aLNEJtzwtDZV1xQxAKA==
@ -14844,7 +14833,7 @@ ember-cli@3.24.0:
workerpool "^6.0.3"
yam "^1.0.0"
ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.0, ember-compatibility-helpers@^1.2.1, ember-compatibility-helpers@^1.2.4, ember-compatibility-helpers@^1.2.5:
ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.0, ember-compatibility-helpers@^1.2.1, ember-compatibility-helpers@^1.2.5:
version "1.2.7"
resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.7.tgz#b4f138bba844f8f38f0b8f4d7e928841cd5e6591"
integrity sha512-BtkjulweiXo9c3yVWrtexw2dTmBrvavD/xixNC6TKOBdrixUwU+6nuOO9dufDWsMxoid7MvtmDpzc9+mE8PdaA==
@ -14969,7 +14958,7 @@ ember-drag-drop@0.4.8:
dependencies:
ember-cli-babel "^6.6.0"
ember-element-helper@^0.5.0, ember-element-helper@^0.5.5:
ember-element-helper@^0.5.0:
version "0.5.5"
resolved "https://registry.yarnpkg.com/ember-element-helper/-/ember-element-helper-0.5.5.tgz#4a9ecb4dce57ee7f5ceb868a53c7b498c729f056"
integrity sha512-Tu3hsI+/mjHBUvw62Qi+YDZtKkn59V66CjwbgfNTZZ7aHf4gFm1ow4zJ4WLnpnie8p9FvOmIUxwl5HvgPJIcFA==
@ -15080,7 +15069,7 @@ ember-in-element-polyfill@^1.0.1:
ember-cli-htmlbars "^5.3.1"
ember-cli-version-checker "^5.1.2"
ember-in-viewport@4.1.0:
ember-in-viewport@4.1.0, ember-in-viewport@~3.10.2:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ember-in-viewport/-/ember-in-viewport-4.1.0.tgz#a9359a1e4a99d9d6ab32e926749dc131084ed896"
integrity sha512-3y6qWXuJPPc6vX2GfxWgtr+sDjb+bdZF9babstr0lTd8t8c1b42gJ13GaJqlylZIyZz2dEXFCimX9WAeudPv9g==
@ -15094,18 +15083,6 @@ ember-in-viewport@4.1.0:
intersection-observer-admin "~0.3.2"
raf-pool "~0.1.4"
ember-in-viewport@~3.10.2:
version "3.10.3"
resolved "https://registry.yarnpkg.com/ember-in-viewport/-/ember-in-viewport-3.10.3.tgz#317472bb82bed11f7895821b799349c6a7406e81"
integrity sha512-hSX7p+G6hJjZaY2BAqzyuiMP7QIHzQ4g0+ZBnEwAa8GMbILFAtzPx5A4XEX8wY6dSzhHB9n9jkcWZdmaML6q8A==
dependencies:
ember-auto-import "^1.11.2"
ember-cli-babel "^7.26.3"
ember-modifier "^2.1.0"
fast-deep-equal "^2.0.1"
intersection-observer-admin "~0.3.2"
raf-pool "~0.1.4"
ember-infinity@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/ember-infinity/-/ember-infinity-2.3.0.tgz#73fe13883c212147bfba4f0b2fe8c8d2a96887d9"
@ -15208,20 +15185,7 @@ ember-modifier@4.1.0, "ember-modifier@^2.1.2 || ^3.0.0 || ^4.0.0", "ember-modifi
ember-cli-normalize-entity-name "^1.0.0"
ember-cli-string-utils "^1.1.0"
ember-modifier@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-2.1.2.tgz#62d18faedf972dcd9d34f90d5321fbc943d139b1"
integrity sha512-3Lsu1fV1sIGa66HOW07RZc6EHISwKt5VA5AUnFss2HX6OTfpxTJ2qvPctt2Yt0XPQXJ4G6BQasr/F35CX7UGJA==
dependencies:
ember-cli-babel "^7.22.1"
ember-cli-normalize-entity-name "^1.0.0"
ember-cli-string-utils "^1.1.0"
ember-cli-typescript "^3.1.3"
ember-compatibility-helpers "^1.2.4"
ember-destroyable-polyfill "^2.0.2"
ember-modifier-manager-polyfill "^1.2.0"
ember-modifier@^3.0.0, ember-modifier@^3.2.7:
ember-modifier@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b"
integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==
@ -15379,14 +15343,6 @@ ember-source@3.24.0:
semver "^6.1.1"
silent-error "^1.1.1"
ember-style-modifier@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/ember-style-modifier/-/ember-style-modifier-0.7.0.tgz#85b3dfd7e4bc2bd546df595f2dab4fb141cf7d87"
integrity sha512-iDzffiwJcb9j6gu3g8CxzZOTvRZ0BmLMEFl+uyqjiaj72VVND9+HbLyQRw1/ewPAtinhSktxxTTdwU/JO+stLw==
dependencies:
ember-cli-babel "^7.26.6"
ember-modifier "^3.0.0"
"ember-style-modifier@^0.8.0 || ^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/ember-style-modifier/-/ember-style-modifier-1.0.0.tgz#96e5d342a255d8c1cba1637779dbb1949322e139"
@ -15556,6 +15512,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@ -17200,6 +17161,11 @@ flexsearch@0.7.21:
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.21.tgz#0f5ede3f2aae67ddc351efbe3b24b69d29e9d48b"
integrity sha512-W7cHV7Hrwjid6lWmy0IhsWDFQboWSng25U3VVywpHOTJnnAZNPScog67G+cVpeX9f7yDD21ih0WDrMMT+JoaYg==
flexsearch@0.7.43:
version "0.7.43"
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.43.tgz#34f89b36278a466ce379c5bf6fb341965ed3f16c"
integrity sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==
flow-parser@0.*:
version "0.205.1"
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.205.1.tgz#337464aaf027b00b2514610386cf21a5f7c94137"
@ -19943,12 +19909,12 @@ iterate-value@^1.0.2:
es-get-iterator "^1.0.2"
iterate-iterator "^1.0.1"
jackspeak@2.1.1, jackspeak@^2.3.5:
version "2.1.1"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd"
integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw==
jackspeak@^2.3.5:
version "2.3.6"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
dependencies:
cliui "^8.0.1"
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
@ -28208,6 +28174,15 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@ -28217,15 +28192,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@ -28234,6 +28200,15 @@ string-width@^2.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
dependencies:
eastasianwidth "^0.2.0"
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
string-width@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a"
@ -28314,6 +28289,13 @@ stringify-entities@^2.0.0:
is-decimal "^1.0.2"
is-hexadecimal "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@ -28335,14 +28317,7 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.1.0:
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
@ -30780,6 +30755,15 @@ workerpool@^6.0.2, workerpool@^6.0.3, workerpool@^6.1.5, workerpool@^6.4.0:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544"
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@ -30789,14 +30773,14 @@ wrap-ansi@^6.0.1:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
ansi-styles "^6.1.0"
string-width "^5.0.1"
strip-ansi "^7.0.1"
wrap-ansi@^9.0.0:
version "9.0.0"