Move the code related to plugin loading / installation to "flipper-plugin-lib"
Summary: Sorry for so long diff, but actually there are no functional changes, just refactoring to make further changes of Plugin Manager easier to understand. I've de-coupled the code related to plugin management from UI code and moved it from PluginInstaller UI component (which will be replaced soon by new UI) to "flipper-plugin-lib". So pretty much everything related to plugin discovery and installation now consolidated in this package. Additionally, this refactoring enables re-using of plugin management code in "flipper-pkg", e.g. to create CLI command for plugin installation from NPM, e.g.: `flipper-pkg install flipper-plugin-reactotron`. Reviewed By: passy Differential Revision: D23679346 fbshipit-source-id: 82e7b9de9afa08c508c1b228c2038b4ba423571c
This commit is contained in:
committed by
Facebook GitHub Bot
parent
72ff87d7cd
commit
e48707151a
@@ -10,7 +10,6 @@
|
||||
"privileged": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@algolia/client-search": "4.3.0",
|
||||
"@emotion/core": "^10.0.22",
|
||||
"@emotion/styled": "^10.0.23",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -19,7 +18,6 @@
|
||||
"JSONStream": "^1.3.1",
|
||||
"adbkit": "^2.11.1",
|
||||
"adbkit-logcat": "^2.0.1",
|
||||
"algoliasearch": "^4.0.0",
|
||||
"archiver": "^5.0.0",
|
||||
"async-mutex": "^0.1.3",
|
||||
"axios": "^0.19.2",
|
||||
@@ -37,7 +35,6 @@
|
||||
"invariant": "^2.2.2",
|
||||
"lodash": "^4.17.19",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"npm-api": "^1.0.0",
|
||||
"open": "^7.0.0",
|
||||
"openssl-wrapper": "^0.3.4",
|
||||
"promise-retry": "^1.1.1",
|
||||
|
||||
@@ -25,24 +25,19 @@ import {
|
||||
LoadingIndicator,
|
||||
Tooltip,
|
||||
} from 'flipper';
|
||||
import React, {useCallback, useState, useMemo, useEffect} from 'react';
|
||||
import React, {useCallback, useState, useEffect} from 'react';
|
||||
import {List} from 'immutable';
|
||||
import {SearchIndex} from 'algoliasearch';
|
||||
import {SearchResponse} from '@algolia/client-search';
|
||||
import {reportPlatformFailures, reportUsage} from '../../utils/metrics';
|
||||
import restartFlipper from '../../utils/restartFlipper';
|
||||
import {registerInstalledPlugins} from '../../reducers/pluginManager';
|
||||
import {
|
||||
getPendingAndInstalledPlugins,
|
||||
removePlugin,
|
||||
PluginMap,
|
||||
PluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {
|
||||
provideSearchIndex,
|
||||
findPluginUpdates as _findPluginUpdates,
|
||||
UpdateResult,
|
||||
} from '../../utils/pluginManager';
|
||||
getInstalledPlugins,
|
||||
getUpdatablePlugins,
|
||||
removePlugin,
|
||||
UpdatablePluginDetails,
|
||||
InstalledPluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {installPluginFromNpm} from 'flipper-plugin-lib';
|
||||
import {State as AppState} from '../../reducers';
|
||||
import {connect} from 'react-redux';
|
||||
@@ -97,7 +92,7 @@ const RestartBar = styled(FlexColumn)({
|
||||
});
|
||||
|
||||
type PropsFromState = {
|
||||
installedPlugins: PluginMap;
|
||||
installedPlugins: InstalledPluginDetails[];
|
||||
};
|
||||
|
||||
type DispatchFromProps = {
|
||||
@@ -105,58 +100,29 @@ type DispatchFromProps = {
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
searchIndexFactory: () => SearchIndex;
|
||||
autoHeight: boolean;
|
||||
findPluginUpdates: (
|
||||
currentPlugins: PluginMap,
|
||||
) => Promise<[string, UpdateResult][]>;
|
||||
};
|
||||
|
||||
type Props = OwnProps & PropsFromState & DispatchFromProps;
|
||||
|
||||
const defaultProps: OwnProps = {
|
||||
searchIndexFactory: provideSearchIndex,
|
||||
autoHeight: false,
|
||||
findPluginUpdates: _findPluginUpdates,
|
||||
};
|
||||
|
||||
type UpdatablePlugin = {
|
||||
updateStatus: UpdateResult;
|
||||
};
|
||||
|
||||
type UpdatablePluginDefinition = PluginDetails & UpdatablePlugin;
|
||||
|
||||
// exported for testing
|
||||
export function annotatePluginsWithUpdates(
|
||||
installedPlugins: PluginMap,
|
||||
updates: Map<string, UpdateResult>,
|
||||
): Map<string, UpdatablePluginDefinition> {
|
||||
const annotated: Array<[string, UpdatablePluginDefinition]> = Array.from(
|
||||
installedPlugins.entries(),
|
||||
).map(([key, value]) => {
|
||||
const updateStatus = updates.get(key) || {kind: 'up-to-date'};
|
||||
return [key, {...value, updateStatus: updateStatus}];
|
||||
});
|
||||
return new Map(annotated);
|
||||
}
|
||||
|
||||
const PluginInstaller = function (props: Props) {
|
||||
const PluginInstaller = function ({
|
||||
refreshInstalledPlugins,
|
||||
installedPlugins,
|
||||
autoHeight,
|
||||
}: Props) {
|
||||
const [restartRequired, setRestartRequired] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const onInstall = useCallback(async () => {
|
||||
props.refreshInstalledPlugins();
|
||||
refreshInstalledPlugins();
|
||||
setRestartRequired(true);
|
||||
}, []);
|
||||
}, [refreshInstalledPlugins]);
|
||||
|
||||
const rows = useNPMSearch(
|
||||
query,
|
||||
setQuery,
|
||||
props.searchIndexFactory,
|
||||
props.installedPlugins,
|
||||
onInstall,
|
||||
props.findPluginUpdates,
|
||||
);
|
||||
const rows = useNPMSearch(query, onInstall, installedPlugins);
|
||||
const restartApp = useCallback(() => {
|
||||
restartFlipper();
|
||||
}, []);
|
||||
@@ -187,7 +153,7 @@ const PluginInstaller = function (props: Props) {
|
||||
columns={columns}
|
||||
highlightableRows={false}
|
||||
highlightedRows={new Set()}
|
||||
autoHeight={props.autoHeight}
|
||||
autoHeight={autoHeight}
|
||||
rows={rows}
|
||||
/>
|
||||
</Container>
|
||||
@@ -195,7 +161,6 @@ const PluginInstaller = function (props: Props) {
|
||||
</>
|
||||
);
|
||||
};
|
||||
PluginInstaller.defaultProps = defaultProps;
|
||||
|
||||
const TableButton = styled(Button)({
|
||||
marginTop: 2,
|
||||
@@ -209,18 +174,10 @@ const AlignedGlyph = styled(Glyph)({
|
||||
marginTop: 6,
|
||||
});
|
||||
|
||||
function liftUpdatable(val: PluginDetails): UpdatablePluginDefinition {
|
||||
return {
|
||||
...val,
|
||||
updateStatus: {kind: 'up-to-date'},
|
||||
};
|
||||
}
|
||||
|
||||
function InstallButton(props: {
|
||||
name: string;
|
||||
version: string;
|
||||
onInstall: () => void;
|
||||
installed: boolean;
|
||||
updateStatus: UpdateResult;
|
||||
}) {
|
||||
type InstallAction =
|
||||
@@ -280,9 +237,9 @@ function InstallButton(props: {
|
||||
const [action, setAction] = useState<InstallAction>(
|
||||
props.updateStatus.kind === 'update-available'
|
||||
? {kind: 'Update'}
|
||||
: props.installed
|
||||
? {kind: 'Remove'}
|
||||
: {kind: 'Install'},
|
||||
: props.updateStatus.kind === 'not-installed'
|
||||
? {kind: 'Install'}
|
||||
: {kind: 'Remove'},
|
||||
);
|
||||
|
||||
if (action.kind === 'Waiting') {
|
||||
@@ -332,22 +289,19 @@ function InstallButton(props: {
|
||||
|
||||
function useNPMSearch(
|
||||
query: string,
|
||||
setQuery: (query: string) => void,
|
||||
searchClientFactory: () => SearchIndex,
|
||||
installedPlugins: PluginMap,
|
||||
onInstall: () => Promise<void>,
|
||||
findPluginUpdates: (
|
||||
currentPlugins: PluginMap,
|
||||
) => Promise<[string, UpdateResult][]>,
|
||||
onInstall: () => void,
|
||||
installedPlugins: InstalledPluginDetails[],
|
||||
): TableRows_immutable {
|
||||
const index = useMemo(searchClientFactory, []);
|
||||
|
||||
useEffect(() => {
|
||||
reportUsage(`${TAG}:open`);
|
||||
}, []);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<UpdatablePluginDetails[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const createRow = useCallback(
|
||||
(h: UpdatablePluginDefinition) => ({
|
||||
(h: UpdatablePluginDetails) => ({
|
||||
key: h.name,
|
||||
columns: {
|
||||
name: {
|
||||
@@ -378,7 +332,6 @@ function useNPMSearch(
|
||||
name={h.name}
|
||||
version={h.version}
|
||||
onInstall={onInstall}
|
||||
installed={installedPlugins.has(h.name)}
|
||||
updateStatus={h.updateStatus}
|
||||
/>
|
||||
),
|
||||
@@ -386,37 +339,20 @@ function useNPMSearch(
|
||||
},
|
||||
},
|
||||
}),
|
||||
[installedPlugins],
|
||||
[onInstall],
|
||||
);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
UpdatablePluginDefinition[]
|
||||
>([]);
|
||||
const [
|
||||
updateAnnotatedInstalledPlugins,
|
||||
setUpdateAnnotatedInstalledPlugins,
|
||||
] = useState<Map<string, UpdatablePluginDefinition>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let cancelled = false;
|
||||
const {hits} = await reportPlatformFailures(
|
||||
index.search<PluginDetails>('', {
|
||||
query,
|
||||
filters: 'keywords:flipper-plugin',
|
||||
hitsPerPage: 20,
|
||||
}) as Promise<SearchResponse<PluginDetails>>,
|
||||
const updatablePlugins = await reportPlatformFailures(
|
||||
getUpdatablePlugins(),
|
||||
`${TAG}:queryIndex`,
|
||||
);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setSearchResults(
|
||||
hits
|
||||
.filter((hit) => !installedPlugins.has(hit.name))
|
||||
.map(liftUpdatable),
|
||||
);
|
||||
|
||||
setSearchResults(updatablePlugins);
|
||||
// Clean up: if query changes while we're searching, abandon results.
|
||||
return () => {
|
||||
cancelled = true;
|
||||
@@ -424,28 +360,19 @@ function useNPMSearch(
|
||||
})();
|
||||
}, [query, installedPlugins]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const updates = new Map(await findPluginUpdates(installedPlugins));
|
||||
setUpdateAnnotatedInstalledPlugins(
|
||||
annotatePluginsWithUpdates(installedPlugins, updates),
|
||||
);
|
||||
})();
|
||||
}, [installedPlugins]);
|
||||
|
||||
const results = Array.from(updateAnnotatedInstalledPlugins.values()).concat(
|
||||
searchResults,
|
||||
);
|
||||
return List(results.map(createRow));
|
||||
const rows: TableRows_immutable = List(searchResults.map(createRow));
|
||||
return rows;
|
||||
}
|
||||
|
||||
PluginInstaller.defaultProps = defaultProps;
|
||||
|
||||
export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
|
||||
({pluginManager: {installedPlugins}}) => ({
|
||||
installedPlugins,
|
||||
}),
|
||||
(dispatch: Dispatch<Action<any>>) => ({
|
||||
refreshInstalledPlugins: () => {
|
||||
getPendingAndInstalledPlugins().then((plugins) =>
|
||||
getInstalledPlugins().then((plugins) =>
|
||||
dispatch(registerInstalledPlugins(plugins)),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -7,84 +7,109 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {annotatePluginsWithUpdates} from '../PluginInstaller';
|
||||
import {UpdateResult} from '../../../utils/pluginManager';
|
||||
import {PluginDetails} from 'flipper-plugin-lib';
|
||||
jest.mock('flipper-plugin-lib');
|
||||
|
||||
test('annotatePluginsWithUpdates', async () => {
|
||||
const installedPlugins = new Map<string, PluginDetails>([
|
||||
[
|
||||
'example',
|
||||
{
|
||||
name: 'example',
|
||||
version: '0.1.0',
|
||||
description: 'Gaze into the death crystal',
|
||||
dir: '/plugins/example',
|
||||
specVersion: 2,
|
||||
source: 'src/index.ts',
|
||||
isDefault: false,
|
||||
main: 'lib/index.js',
|
||||
title: 'Example',
|
||||
id: 'Example',
|
||||
entry: '/plugins/example/lib/index.js',
|
||||
},
|
||||
],
|
||||
[
|
||||
'ricksybusiness',
|
||||
{
|
||||
name: 'ricksybusiness',
|
||||
version: '1.0.0',
|
||||
description: 'Rick Die Rickpeat',
|
||||
dir: '/plugins/example',
|
||||
specVersion: 2,
|
||||
source: 'src/index.ts',
|
||||
isDefault: false,
|
||||
main: 'lib/index.js',
|
||||
title: 'ricksybusiness',
|
||||
id: 'ricksybusiness',
|
||||
entry: '/plugins/ricksybusiness/lib/index.js',
|
||||
},
|
||||
],
|
||||
]);
|
||||
const updates = new Map<string, UpdateResult>([
|
||||
['example', {kind: 'update-available', version: '1.1.0'}],
|
||||
]);
|
||||
const res = annotatePluginsWithUpdates(installedPlugins, updates);
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
"example" => Object {
|
||||
"description": "Gaze into the death crystal",
|
||||
"dir": "/plugins/example",
|
||||
"entry": "/plugins/example/lib/index.js",
|
||||
"id": "Example",
|
||||
"isDefault": false,
|
||||
"main": "lib/index.js",
|
||||
"name": "example",
|
||||
"source": "src/index.ts",
|
||||
"specVersion": 2,
|
||||
"title": "Example",
|
||||
"updateStatus": Object {
|
||||
"kind": "update-available",
|
||||
"version": "1.1.0",
|
||||
},
|
||||
"version": "0.1.0",
|
||||
},
|
||||
"ricksybusiness" => Object {
|
||||
"description": "Rick Die Rickpeat",
|
||||
"dir": "/plugins/example",
|
||||
"entry": "/plugins/ricksybusiness/lib/index.js",
|
||||
"id": "ricksybusiness",
|
||||
"isDefault": false,
|
||||
"main": "lib/index.js",
|
||||
"name": "ricksybusiness",
|
||||
"source": "src/index.ts",
|
||||
"specVersion": 2,
|
||||
"title": "ricksybusiness",
|
||||
"updateStatus": Object {
|
||||
"kind": "up-to-date",
|
||||
},
|
||||
"version": "1.0.0",
|
||||
},
|
||||
}
|
||||
`);
|
||||
import {default as PluginInstaller} from '../PluginInstaller';
|
||||
import React from 'react';
|
||||
import {render, waitForElement} from '@testing-library/react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {Provider} from 'react-redux';
|
||||
import type {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
import {getUpdatablePlugins, UpdatablePluginDetails} from 'flipper-plugin-lib';
|
||||
import {Store} from '../../../reducers';
|
||||
import {mocked} from 'ts-jest/utils';
|
||||
|
||||
const getUpdatablePluginsMock = mocked(getUpdatablePlugins);
|
||||
|
||||
function getStore(installedPlugins: InstalledPluginDetails[] = []): Store {
|
||||
return configureStore([])({
|
||||
application: {sessionId: 'mysession'},
|
||||
pluginManager: {installedPlugins},
|
||||
}) as Store;
|
||||
}
|
||||
|
||||
const samplePluginDetails1: UpdatablePluginDetails = {
|
||||
name: 'flipper-plugin-hello',
|
||||
entry: './test/index.js',
|
||||
version: '0.1.0',
|
||||
specVersion: 2,
|
||||
main: 'dist/bundle.js',
|
||||
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample1',
|
||||
source: 'src/index.js',
|
||||
id: 'Hello',
|
||||
title: 'Hello',
|
||||
description: 'World?',
|
||||
isDefault: false,
|
||||
updateStatus: {
|
||||
kind: 'not-installed',
|
||||
version: '0.1.0',
|
||||
},
|
||||
};
|
||||
|
||||
const samplePluginDetails2: UpdatablePluginDetails = {
|
||||
name: 'flipper-plugin-world',
|
||||
entry: './test/index.js',
|
||||
version: '0.2.0',
|
||||
specVersion: 2,
|
||||
main: 'dist/bundle.js',
|
||||
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample2',
|
||||
source: 'src/index.js',
|
||||
id: 'World',
|
||||
title: 'World',
|
||||
description: 'Hello?',
|
||||
isDefault: false,
|
||||
updateStatus: {
|
||||
kind: 'not-installed',
|
||||
version: '0.2.0',
|
||||
},
|
||||
};
|
||||
|
||||
const SEARCH_RESULTS = [samplePluginDetails1, samplePluginDetails2];
|
||||
|
||||
afterEach(() => {
|
||||
getUpdatablePluginsMock.mockClear();
|
||||
});
|
||||
|
||||
test('load PluginInstaller list', async () => {
|
||||
getUpdatablePluginsMock.mockReturnValue(Promise.resolve(SEARCH_RESULTS));
|
||||
const component = (
|
||||
<Provider store={getStore()}>
|
||||
<PluginInstaller
|
||||
// Bit ugly to have this as an effectively test-only option, but
|
||||
// without, we rely on height information from Electron which we don't
|
||||
// have, causing no items to be rendered.
|
||||
autoHeight={true}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
const {container, getByText} = render(component);
|
||||
await waitForElement(() => getByText('hello'));
|
||||
expect(getUpdatablePluginsMock.mock.calls.length).toBe(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('load PluginInstaller list with one plugin installed', async () => {
|
||||
getUpdatablePluginsMock.mockReturnValue(
|
||||
Promise.resolve([
|
||||
{...samplePluginDetails1, updateStatus: {kind: 'up-to-date'}},
|
||||
samplePluginDetails2,
|
||||
]),
|
||||
);
|
||||
const store = getStore([
|
||||
{...samplePluginDetails1, installationStatus: 'installed'},
|
||||
]);
|
||||
const component = (
|
||||
<Provider store={store}>
|
||||
<PluginInstaller
|
||||
// Bit ugly to have this as an effectively test-only option, but
|
||||
// without, we rely on height information from Electron which we don't
|
||||
// have, causing no items to be rendered.
|
||||
autoHeight={true}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
const {container, getByText} = render(component);
|
||||
await waitForElement(() => getByText('hello'));
|
||||
expect(getUpdatablePluginsMock.mock.calls.length).toBe(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`load PluginInstaller list 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-13jp8bd-View-FlexBox-FlexColumn"
|
||||
>
|
||||
<div
|
||||
class="css-mx54fh-View-FlexBox-FlexRow-Toolbar e13mj6h80"
|
||||
>
|
||||
<div
|
||||
class="css-awcbnc-View-FlexBox-SearchBox e271nro1"
|
||||
>
|
||||
<input
|
||||
class="css-mquw9q-Input-SearchInput e271nro2"
|
||||
placeholder="Search Flipper plugins..."
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1s2tz3g-View-FlexBox-FlexColumn-Container e11hk09w0"
|
||||
>
|
||||
<div
|
||||
class="css-k28k5g-View-FlexBox-FlexColumn ecr18to0"
|
||||
>
|
||||
<div
|
||||
class="css-guwxm6-View-FlexBox-FlexRow-TableHeadContainer ejga3103"
|
||||
>
|
||||
<div
|
||||
class="css-zk363n-TableHeadColumnContainer ejga3104"
|
||||
title="name"
|
||||
width="25%"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
Name
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1243222-TableHeadColumnContainer ejga3104"
|
||||
title="version"
|
||||
width="10%"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
Version
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-nyra6d-TableHeadColumnContainer ejga3104"
|
||||
title="description"
|
||||
width="flex"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-hyjqmu-TableHeadColumnContainer ejga3104"
|
||||
title="install"
|
||||
width="15%"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1s2tz3g-View-FlexBox-FlexColumn-Container e11hk09w0"
|
||||
>
|
||||
<div
|
||||
class="css-1gi2nc1-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum0"
|
||||
data-key="flipper-plugin-hello"
|
||||
>
|
||||
<div
|
||||
class="css-hdg6vt-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="25%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
hello
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1rdsust-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="10%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
0.1.0
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1bw2bjn-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="flex"
|
||||
>
|
||||
<div
|
||||
class="css-wxxfdj-View-FlexBox-FlexRow epz0qe20"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
World?
|
||||
</span>
|
||||
<div
|
||||
class="css-te359u-View-FlexBox-Spacer e13mj6h81"
|
||||
/>
|
||||
<span
|
||||
class="css-ad6n9d-StyledLink e1mzoj7l0"
|
||||
>
|
||||
<div
|
||||
class="css-1tfjvvq-ColoredIconCustom e528dze1"
|
||||
color="#bec2c9"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-4dx1ef-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="15%"
|
||||
>
|
||||
<div
|
||||
class="css-1pccuw6-StyledButton enfqd40"
|
||||
type="primary"
|
||||
>
|
||||
Install
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-14d4692-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum0"
|
||||
data-key="flipper-plugin-world"
|
||||
>
|
||||
<div
|
||||
class="css-hdg6vt-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="25%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
world
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1rdsust-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="10%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
0.2.0
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1bw2bjn-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="flex"
|
||||
>
|
||||
<div
|
||||
class="css-wxxfdj-View-FlexBox-FlexRow epz0qe20"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
Hello?
|
||||
</span>
|
||||
<div
|
||||
class="css-te359u-View-FlexBox-Spacer e13mj6h81"
|
||||
/>
|
||||
<span
|
||||
class="css-ad6n9d-StyledLink e1mzoj7l0"
|
||||
>
|
||||
<div
|
||||
class="css-1tfjvvq-ColoredIconCustom e528dze1"
|
||||
color="#bec2c9"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-4dx1ef-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="15%"
|
||||
>
|
||||
<div
|
||||
class="css-1pccuw6-StyledButton enfqd40"
|
||||
type="primary"
|
||||
>
|
||||
Install
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-mx54fh-View-FlexBox-FlexRow-Toolbar e13mj6h80"
|
||||
>
|
||||
<div
|
||||
class="css-1stmykz-View-FlexBox-FlexRow-Container ersmi541"
|
||||
>
|
||||
<input
|
||||
class="css-phqpi-Input-FileInputBox ersmi543"
|
||||
placeholder="Specify path to a Flipper package or just drag and drop it here..."
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="css-1juns87-View-FlexBox-FlexRow-GlyphContainer ersmi542"
|
||||
>
|
||||
<img
|
||||
alt="dots-3-circle"
|
||||
class="ersmi540 css-6iptsk-ColoredIconBlack-CenteredGlyph e528dze0"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=dots-3-circle&variant=outline&size=16&set=facebook_icons&density=1x"
|
||||
title="Open file selection dialog"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-1juns87-View-FlexBox-FlexRow-GlyphContainer ersmi542"
|
||||
>
|
||||
<div
|
||||
class="css-yzqaun-TooltipContainer e1abcqbd0"
|
||||
>
|
||||
<div
|
||||
class="ersmi540 css-1vos16e-ColoredIconCustom-CenteredGlyph e528dze1"
|
||||
color="#D79651"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=caution-triangle&variant=filled&size=16&set=facebook_icons&density=1x"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-16kzr4q-View-FlexBox-FlexRow-ButtonContainer evd5j492"
|
||||
>
|
||||
<div
|
||||
class="css-17wo7w2-View-FlexBox-FlexRow epz0qe20"
|
||||
>
|
||||
<div
|
||||
class="css-1rxpsn8-StyledButton enfqd40"
|
||||
disabled=""
|
||||
title="Cannot install plugin package by the specified path"
|
||||
type="primary"
|
||||
>
|
||||
Install
|
||||
</div>
|
||||
<div
|
||||
class="css-ywl8lj-View-FlexBox-FlexRow-ErrorGlyphContainer evd5j493"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`load PluginInstaller list with one plugin installed 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-13jp8bd-View-FlexBox-FlexColumn"
|
||||
>
|
||||
<div
|
||||
class="css-mx54fh-View-FlexBox-FlexRow-Toolbar e13mj6h80"
|
||||
>
|
||||
<div
|
||||
class="css-awcbnc-View-FlexBox-SearchBox e271nro1"
|
||||
>
|
||||
<input
|
||||
class="css-mquw9q-Input-SearchInput e271nro2"
|
||||
placeholder="Search Flipper plugins..."
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1s2tz3g-View-FlexBox-FlexColumn-Container e11hk09w0"
|
||||
>
|
||||
<div
|
||||
class="css-k28k5g-View-FlexBox-FlexColumn ecr18to0"
|
||||
>
|
||||
<div
|
||||
class="css-guwxm6-View-FlexBox-FlexRow-TableHeadContainer ejga3103"
|
||||
>
|
||||
<div
|
||||
class="css-zk363n-TableHeadColumnContainer ejga3104"
|
||||
title="name"
|
||||
width="25%"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
Name
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1243222-TableHeadColumnContainer ejga3104"
|
||||
title="version"
|
||||
width="10%"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
Version
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-nyra6d-TableHeadColumnContainer ejga3104"
|
||||
title="description"
|
||||
width="flex"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-hyjqmu-TableHeadColumnContainer ejga3104"
|
||||
title="install"
|
||||
width="15%"
|
||||
>
|
||||
<div
|
||||
class="ejga3101 css-1859yug-InteractiveContainer-TableHeaderColumnInteractive etmd34w0"
|
||||
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-1yopr4n-TableHeaderColumnContainer ejga3102"
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1s2tz3g-View-FlexBox-FlexColumn-Container e11hk09w0"
|
||||
>
|
||||
<div
|
||||
class="css-1gi2nc1-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum0"
|
||||
data-key="flipper-plugin-hello"
|
||||
>
|
||||
<div
|
||||
class="css-hdg6vt-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="25%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
hello
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1rdsust-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="10%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
0.1.0
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1bw2bjn-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="flex"
|
||||
>
|
||||
<div
|
||||
class="css-wxxfdj-View-FlexBox-FlexRow epz0qe20"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
World?
|
||||
</span>
|
||||
<div
|
||||
class="css-te359u-View-FlexBox-Spacer e13mj6h81"
|
||||
/>
|
||||
<span
|
||||
class="css-ad6n9d-StyledLink e1mzoj7l0"
|
||||
>
|
||||
<div
|
||||
class="css-1tfjvvq-ColoredIconCustom e528dze1"
|
||||
color="#bec2c9"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-4dx1ef-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="15%"
|
||||
>
|
||||
<div
|
||||
class="css-1pccuw6-StyledButton enfqd40"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-14d4692-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum0"
|
||||
data-key="flipper-plugin-world"
|
||||
>
|
||||
<div
|
||||
class="css-hdg6vt-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="25%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
world
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1rdsust-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="10%"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
0.2.0
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-1bw2bjn-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="flex"
|
||||
>
|
||||
<div
|
||||
class="css-wxxfdj-View-FlexBox-FlexRow epz0qe20"
|
||||
>
|
||||
<span
|
||||
class="css-fyize4-Text"
|
||||
>
|
||||
Hello?
|
||||
</span>
|
||||
<div
|
||||
class="css-te359u-View-FlexBox-Spacer e13mj6h81"
|
||||
/>
|
||||
<span
|
||||
class="css-ad6n9d-StyledLink e1mzoj7l0"
|
||||
>
|
||||
<div
|
||||
class="css-1tfjvvq-ColoredIconCustom e528dze1"
|
||||
color="#bec2c9"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-4dx1ef-TableBodyColumnContainer ehuguum1"
|
||||
title=""
|
||||
width="15%"
|
||||
>
|
||||
<div
|
||||
class="css-1pccuw6-StyledButton enfqd40"
|
||||
type="primary"
|
||||
>
|
||||
Install
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-mx54fh-View-FlexBox-FlexRow-Toolbar e13mj6h80"
|
||||
>
|
||||
<div
|
||||
class="css-1stmykz-View-FlexBox-FlexRow-Container ersmi541"
|
||||
>
|
||||
<input
|
||||
class="css-phqpi-Input-FileInputBox ersmi543"
|
||||
placeholder="Specify path to a Flipper package or just drag and drop it here..."
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="css-1juns87-View-FlexBox-FlexRow-GlyphContainer ersmi542"
|
||||
>
|
||||
<img
|
||||
alt="dots-3-circle"
|
||||
class="ersmi540 css-6iptsk-ColoredIconBlack-CenteredGlyph e528dze0"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=dots-3-circle&variant=outline&size=16&set=facebook_icons&density=1x"
|
||||
title="Open file selection dialog"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-1juns87-View-FlexBox-FlexRow-GlyphContainer ersmi542"
|
||||
>
|
||||
<div
|
||||
class="css-yzqaun-TooltipContainer e1abcqbd0"
|
||||
>
|
||||
<div
|
||||
class="ersmi540 css-1vos16e-ColoredIconCustom-CenteredGlyph e528dze1"
|
||||
color="#D79651"
|
||||
size="16"
|
||||
src="https://external.xx.fbcdn.net/assets/?name=caution-triangle&variant=filled&size=16&set=facebook_icons&density=1x"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-16kzr4q-View-FlexBox-FlexRow-ButtonContainer evd5j492"
|
||||
>
|
||||
<div
|
||||
class="css-17wo7w2-View-FlexBox-FlexRow epz0qe20"
|
||||
>
|
||||
<div
|
||||
class="css-1rxpsn8-StyledButton enfqd40"
|
||||
disabled=""
|
||||
title="Cannot install plugin package by the specified path"
|
||||
type="primary"
|
||||
>
|
||||
Install
|
||||
</div>
|
||||
<div
|
||||
class="css-ywl8lj-View-FlexBox-FlexRow-ErrorGlyphContainer evd5j493"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -10,10 +10,10 @@
|
||||
import {Store} from '../reducers/index';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
||||
import {getPendingAndInstalledPlugins} from 'flipper-plugin-lib';
|
||||
import {getInstalledPlugins} from 'flipper-plugin-lib';
|
||||
|
||||
function refreshInstalledPlugins(store: Store) {
|
||||
getPendingAndInstalledPlugins().then((plugins) =>
|
||||
getInstalledPlugins().then((plugins) =>
|
||||
store.dispatch(registerInstalledPlugins(plugins)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
*/
|
||||
|
||||
import {default as reducer, registerInstalledPlugins} from '../pluginManager';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
|
||||
test('reduce empty registerInstalledPlugins', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins(new Map()));
|
||||
expect(result).toEqual({installedPlugins: new Map()});
|
||||
const result = reducer(undefined, registerInstalledPlugins([]));
|
||||
expect(result).toEqual({installedPlugins: []});
|
||||
});
|
||||
|
||||
const EXAMPLE_PLUGIN = {
|
||||
@@ -26,17 +27,15 @@ const EXAMPLE_PLUGIN = {
|
||||
title: 'test',
|
||||
id: 'test',
|
||||
entry: '/plugins/test/lib/index.js',
|
||||
};
|
||||
installationStatus: 'installed',
|
||||
} as InstalledPluginDetails;
|
||||
|
||||
test('reduce registerInstalledPlugins, clear again', () => {
|
||||
const result = reducer(
|
||||
undefined,
|
||||
registerInstalledPlugins(new Map([['test', EXAMPLE_PLUGIN]])),
|
||||
);
|
||||
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
||||
expect(result).toEqual({
|
||||
installedPlugins: new Map([['test', EXAMPLE_PLUGIN]]),
|
||||
installedPlugins: [EXAMPLE_PLUGIN],
|
||||
});
|
||||
|
||||
const result2 = reducer(result, registerInstalledPlugins(new Map()));
|
||||
expect(result2).toEqual({installedPlugins: new Map()});
|
||||
const result2 = reducer(result, registerInstalledPlugins([]));
|
||||
expect(result2).toEqual({installedPlugins: []});
|
||||
});
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
*/
|
||||
|
||||
import {Actions} from './';
|
||||
import {PluginMap} from 'flipper-plugin-lib';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
|
||||
export type State = {
|
||||
installedPlugins: PluginMap;
|
||||
installedPlugins: InstalledPluginDetails[];
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||
payload: PluginMap;
|
||||
payload: InstalledPluginDetails[];
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
installedPlugins: new Map(),
|
||||
installedPlugins: [],
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
@@ -37,7 +37,9 @@ export default function reducer(
|
||||
}
|
||||
}
|
||||
|
||||
export const registerInstalledPlugins = (payload: PluginMap): Action => ({
|
||||
export const registerInstalledPlugins = (
|
||||
payload: InstalledPluginDetails[],
|
||||
): Action => ({
|
||||
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import {homedir} from 'os';
|
||||
import {PluginMap, PluginDetails} from 'flipper-plugin-lib';
|
||||
import {default as algoliasearch, SearchIndex} from 'algoliasearch';
|
||||
import NpmApi, {Package} from 'npm-api';
|
||||
import semver from 'semver';
|
||||
|
||||
const ALGOLIA_APPLICATION_ID = 'OFCNCOG2CU';
|
||||
const ALGOLIA_API_KEY = 'f54e21fa3a2a0160595bb058179bfb1e';
|
||||
|
||||
export const PLUGIN_DIR = path.join(homedir(), '.flipper', 'thirdparty');
|
||||
|
||||
// TODO(T57014856): This should be private, too.
|
||||
export function provideSearchIndex(): SearchIndex {
|
||||
const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY);
|
||||
return client.initIndex('npm-search');
|
||||
}
|
||||
|
||||
export type UpdateResult =
|
||||
| {kind: 'up-to-date'}
|
||||
| {kind: 'error'; error: Error}
|
||||
| {kind: 'update-available'; version: string};
|
||||
|
||||
export async function findPluginUpdates(
|
||||
currentPlugins: PluginMap,
|
||||
): Promise<[string, UpdateResult][]> {
|
||||
const npm = new NpmApi();
|
||||
|
||||
return Promise.all(
|
||||
Array.from(currentPlugins.values()).map(
|
||||
async (currentPlugin: PluginDetails): Promise<[string, UpdateResult]> =>
|
||||
npm
|
||||
.repo(currentPlugin.name)
|
||||
.package()
|
||||
.then((pkg: Package): [string, UpdateResult] => {
|
||||
if (semver.lt(currentPlugin.version, pkg.version)) {
|
||||
return [
|
||||
currentPlugin.name,
|
||||
{kind: 'update-available', version: pkg.version},
|
||||
];
|
||||
} else {
|
||||
return [currentPlugin.name, {kind: 'up-to-date'}];
|
||||
}
|
||||
})
|
||||
.catch((err) => [currentPlugin.name, {kind: 'error', error: err}]),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -9,11 +9,15 @@
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/facebook/flipper/issues",
|
||||
"dependencies": {
|
||||
"@algolia/client-search": "^4.4.0",
|
||||
"algoliasearch": "^4.4.0",
|
||||
"decompress": "^4.2.1",
|
||||
"decompress-targz": "^4.1.1",
|
||||
"decompress-unzip": "^4.0.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"live-plugin-manager": "^0.14.1",
|
||||
"npm-api": "^1.0.0",
|
||||
"p-map": "^4.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
|
||||
131
desktop/plugin-lib/src/__tests__/getUpdatablePlugins.node.ts
Normal file
131
desktop/plugin-lib/src/__tests__/getUpdatablePlugins.node.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
jest.mock('../getInstalledPlugins');
|
||||
jest.mock('../getNpmHostedPlugins');
|
||||
|
||||
import {getUpdatablePlugins} from '../getUpdatablePlugins';
|
||||
import {
|
||||
getNpmHostedPlugins,
|
||||
NpmPackageDescriptor,
|
||||
} from '../getNpmHostedPlugins';
|
||||
import type {InstalledPluginDetails} from '../getInstalledPlugins';
|
||||
import {getInstalledPlugins} from '../getInstalledPlugins';
|
||||
import {mocked} from 'ts-jest/utils';
|
||||
import type {Package} from 'npm-api';
|
||||
|
||||
jest.mock('npm-api', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
repo: jest.fn().mockImplementation((name: string) => {
|
||||
let pkg: Package | undefined;
|
||||
if (name === 'flipper-plugin-hello') {
|
||||
pkg = {
|
||||
$schema: 'https://fbflipper.com/schemas/plugin-package/v2.json',
|
||||
name: 'flipper-plugin-hello',
|
||||
title: 'Hello',
|
||||
version: '0.1.0',
|
||||
main: 'dist/bundle.js',
|
||||
flipperBundlerEntry: 'src/index.js',
|
||||
description: 'World?',
|
||||
};
|
||||
} else if (name === 'flipper-plugin-world') {
|
||||
pkg = {
|
||||
$schema: 'https://fbflipper.com/schemas/plugin-package/v2.json',
|
||||
name: 'flipper-plugin-world',
|
||||
title: 'World',
|
||||
version: '0.3.0',
|
||||
main: 'dist/bundle.js',
|
||||
flipperBundlerEntry: 'src/index.js',
|
||||
description: 'World?',
|
||||
};
|
||||
}
|
||||
return {
|
||||
package: jest.fn().mockImplementation(() => Promise.resolve(pkg)),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const installedPlugins: InstalledPluginDetails[] = [
|
||||
{
|
||||
name: 'flipper-plugin-hello',
|
||||
entry: './test/index.js',
|
||||
version: '0.1.0',
|
||||
specVersion: 2,
|
||||
main: 'dist/bundle.js',
|
||||
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample1',
|
||||
source: 'src/index.js',
|
||||
id: 'Hello',
|
||||
title: 'Hello',
|
||||
description: 'World?',
|
||||
isDefault: false,
|
||||
installationStatus: 'installed',
|
||||
},
|
||||
{
|
||||
name: 'flipper-plugin-world',
|
||||
entry: './test/index.js',
|
||||
version: '0.2.0',
|
||||
specVersion: 2,
|
||||
main: 'dist/bundle.js',
|
||||
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample2',
|
||||
source: 'src/index.js',
|
||||
id: 'World',
|
||||
title: 'World',
|
||||
description: 'Hello?',
|
||||
isDefault: false,
|
||||
installationStatus: 'pending',
|
||||
},
|
||||
];
|
||||
|
||||
const updates: NpmPackageDescriptor[] = [
|
||||
{name: 'flipper-plugin-hello', version: '0.1.0'},
|
||||
{name: 'flipper-plugin-world', version: '0.3.0'},
|
||||
];
|
||||
|
||||
test('annotatePluginsWithUpdates', async () => {
|
||||
const getInstalledPluginsMock = mocked(getInstalledPlugins);
|
||||
getInstalledPluginsMock.mockReturnValue(Promise.resolve(installedPlugins));
|
||||
|
||||
const getNpmHostedPluginsMock = mocked(getNpmHostedPlugins);
|
||||
getNpmHostedPluginsMock.mockReturnValue(Promise.resolve(updates));
|
||||
|
||||
const res = await getUpdatablePlugins();
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
expect({
|
||||
name: res[0].name,
|
||||
version: res[0].version,
|
||||
updateStatus: res[0].updateStatus,
|
||||
}).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"name": "flipper-plugin-hello",
|
||||
"updateStatus": Object {
|
||||
"kind": "up-to-date",
|
||||
},
|
||||
"version": "0.1.0",
|
||||
}
|
||||
`);
|
||||
|
||||
expect({
|
||||
name: res[1].name,
|
||||
version: res[1].version,
|
||||
updateStatus: res[1].updateStatus,
|
||||
}).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"name": "flipper-plugin-world",
|
||||
"updateStatus": Object {
|
||||
"kind": "update-available",
|
||||
"version": "0.3.0",
|
||||
},
|
||||
"version": "0.3.0",
|
||||
}
|
||||
`);
|
||||
});
|
||||
104
desktop/plugin-lib/src/getInstalledPlugins.ts
Normal file
104
desktop/plugin-lib/src/getInstalledPlugins.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
import {
|
||||
pluginPendingInstallationDir,
|
||||
pluginInstallationDir,
|
||||
} from './pluginPaths';
|
||||
import PluginDetails from './PluginDetails';
|
||||
import getPluginDetails from './getPluginDetails';
|
||||
import pmap from 'p-map';
|
||||
import {notNull} from './typeUtils';
|
||||
|
||||
export type PluginInstallationStatus =
|
||||
| 'not-installed'
|
||||
| 'installed'
|
||||
| 'pending';
|
||||
|
||||
export type InstalledPluginDetails = PluginDetails & {
|
||||
installationStatus: PluginInstallationStatus;
|
||||
};
|
||||
|
||||
async function getFullyInstalledPlugins(): Promise<PluginDetails[]> {
|
||||
const pluginDirExists = await fs.pathExists(pluginInstallationDir);
|
||||
if (!pluginDirExists) {
|
||||
return [];
|
||||
}
|
||||
const dirs = await fs.readdir(pluginInstallationDir);
|
||||
const plugins = await pmap(dirs, async (dirName) => {
|
||||
const pluginDir = path.join(pluginInstallationDir, dirName);
|
||||
if (!(await fs.lstat(pluginDir)).isDirectory()) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await getPluginDetails(pluginDir);
|
||||
} catch (e) {
|
||||
console.error(`Failed to load plugin from ${pluginDir}`, e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return plugins.filter(notNull);
|
||||
}
|
||||
|
||||
async function getPendingInstallationPlugins(): Promise<PluginDetails[]> {
|
||||
const pluginDirExists = await fs.pathExists(pluginPendingInstallationDir);
|
||||
if (!pluginDirExists) {
|
||||
return [];
|
||||
}
|
||||
const dirs = await fs.readdir(pluginPendingInstallationDir);
|
||||
const plugins = await pmap(dirs, async (dirName) => {
|
||||
const versions = (
|
||||
await fs.readdir(path.join(pluginPendingInstallationDir, dirName))
|
||||
).sort((v1, v2) => semver.compare(v2, v1, true));
|
||||
if (versions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const pluginDir = path.join(
|
||||
pluginPendingInstallationDir,
|
||||
dirName,
|
||||
versions[0],
|
||||
);
|
||||
if (!(await fs.lstat(pluginDir)).isDirectory()) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await getPluginDetails(pluginDir);
|
||||
} catch (e) {
|
||||
console.error(`Failed to load plugin from ${pluginDir}`, e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return plugins.filter(notNull);
|
||||
}
|
||||
|
||||
export async function getInstalledPlugins(): Promise<InstalledPluginDetails[]> {
|
||||
const map = new Map<string, InstalledPluginDetails>(
|
||||
(await getFullyInstalledPlugins()).map((p) => [
|
||||
p.name,
|
||||
{...p, installationStatus: 'installed'},
|
||||
]),
|
||||
);
|
||||
for (const p of await getPendingInstallationPlugins()) {
|
||||
if (!map.get(p.name) || semver.gt(p.version, map.get(p.name)!.version)) {
|
||||
map.set(p.name, {...p, installationStatus: 'pending'});
|
||||
}
|
||||
}
|
||||
const allPlugins = [...map.values()].sort((p1, p2) =>
|
||||
p1.installationStatus === 'installed' && p2.installationStatus === 'pending'
|
||||
? 1
|
||||
: p1.installationStatus === 'pending' &&
|
||||
p2.installationStatus === 'installed'
|
||||
? -1
|
||||
: p1.name.localeCompare(p2.name),
|
||||
);
|
||||
return allPlugins;
|
||||
}
|
||||
46
desktop/plugin-lib/src/getNpmHostedPlugins.ts
Normal file
46
desktop/plugin-lib/src/getNpmHostedPlugins.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {default as algoliasearch, SearchIndex} from 'algoliasearch';
|
||||
|
||||
const ALGOLIA_APPLICATION_ID = 'OFCNCOG2CU';
|
||||
const ALGOLIA_API_KEY = 'f54e21fa3a2a0160595bb058179bfb1e';
|
||||
|
||||
function provideSearchIndex(): SearchIndex {
|
||||
const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY);
|
||||
return client.initIndex('npm-search');
|
||||
}
|
||||
|
||||
export type NpmPackageDescriptor = {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type NpmHostedPluginsSearchArgs = {
|
||||
query?: string;
|
||||
};
|
||||
|
||||
export async function getNpmHostedPlugins(
|
||||
args: NpmHostedPluginsSearchArgs = {},
|
||||
): Promise<NpmPackageDescriptor[]> {
|
||||
const index = provideSearchIndex();
|
||||
args = Object.assign(
|
||||
{
|
||||
query: '',
|
||||
filters: 'keywords:flipper-plugin',
|
||||
hitsPerPage: 50,
|
||||
},
|
||||
args,
|
||||
);
|
||||
const {hits} = await index.search<NpmPackageDescriptor>(
|
||||
args.query || '',
|
||||
args,
|
||||
);
|
||||
return hits;
|
||||
}
|
||||
120
desktop/plugin-lib/src/getUpdatablePlugins.ts
Normal file
120
desktop/plugin-lib/src/getUpdatablePlugins.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import PluginDetails from './PluginDetails';
|
||||
import {getInstalledPlugins} from './getInstalledPlugins';
|
||||
import semver from 'semver';
|
||||
import {getNpmHostedPlugins, NpmPackageDescriptor} from './getNpmHostedPlugins';
|
||||
import NpmApi from 'npm-api';
|
||||
import getPluginDetails from './getPluginDetails';
|
||||
import {getPluginInstallationDir} from './pluginInstaller';
|
||||
import pmap from 'p-map';
|
||||
import {notNull} from './typeUtils';
|
||||
|
||||
export type UpdateResult =
|
||||
| {kind: 'not-installed'; version: string}
|
||||
| {kind: 'pending'}
|
||||
| {kind: 'up-to-date'}
|
||||
| {kind: 'error'; error: Error}
|
||||
| {kind: 'update-available'; version: string};
|
||||
|
||||
export type UpdatablePlugin = {
|
||||
updateStatus: UpdateResult;
|
||||
};
|
||||
|
||||
export type UpdatablePluginDetails = PluginDetails & UpdatablePlugin;
|
||||
|
||||
export async function getUpdatablePlugins(): Promise<UpdatablePluginDetails[]> {
|
||||
const npmApi = new NpmApi();
|
||||
const installedPlugins = await getInstalledPlugins();
|
||||
const npmHostedPlugins = new Map<string, NpmPackageDescriptor>(
|
||||
(await getNpmHostedPlugins()).map((p) => [p.name, p]),
|
||||
);
|
||||
const annotatedInstalledPlugins = await pmap(
|
||||
installedPlugins,
|
||||
async (installedPlugin): Promise<UpdatablePluginDetails> => {
|
||||
try {
|
||||
const npmPackageDescriptor = npmHostedPlugins.get(installedPlugin.name);
|
||||
if (npmPackageDescriptor) {
|
||||
npmHostedPlugins.delete(installedPlugin.name);
|
||||
if (
|
||||
semver.lt(installedPlugin.version, npmPackageDescriptor.version)
|
||||
) {
|
||||
const pkg = await npmApi.repo(npmPackageDescriptor.name).package();
|
||||
const npmPluginDetails = await getPluginDetails(
|
||||
getPluginInstallationDir(npmPackageDescriptor.name),
|
||||
pkg,
|
||||
);
|
||||
return {
|
||||
...npmPluginDetails,
|
||||
updateStatus: {
|
||||
kind: 'update-available',
|
||||
version: npmPluginDetails.version,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const updateStatus: UpdateResult =
|
||||
installedPlugin.installationStatus === 'installed'
|
||||
? {kind: 'up-to-date'}
|
||||
: {kind: 'pending'};
|
||||
return {
|
||||
...installedPlugin,
|
||||
updateStatus,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...installedPlugin,
|
||||
updateStatus: {
|
||||
kind: 'error',
|
||||
error,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 4,
|
||||
},
|
||||
);
|
||||
const annotatedNotInstalledPlugins = await pmap(
|
||||
npmHostedPlugins.values(),
|
||||
async (notInstalledPlugin) => {
|
||||
try {
|
||||
const pkg = await npmApi.repo(notInstalledPlugin.name).package();
|
||||
const npmPluginDetails = await getPluginDetails(
|
||||
getPluginInstallationDir(notInstalledPlugin.name),
|
||||
pkg,
|
||||
);
|
||||
return {
|
||||
...npmPluginDetails,
|
||||
updateStatus: {
|
||||
kind: 'not-installed',
|
||||
version: npmPluginDetails.version,
|
||||
},
|
||||
} as UpdatablePluginDetails;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Failed to load details from npm for plugin ${notInstalledPlugin.name}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 4,
|
||||
},
|
||||
);
|
||||
return [
|
||||
...annotatedInstalledPlugins.sort((p1, p2) =>
|
||||
p1.name.localeCompare(p2.name),
|
||||
),
|
||||
...annotatedNotInstalledPlugins
|
||||
.filter(notNull)
|
||||
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
||||
];
|
||||
}
|
||||
@@ -10,3 +10,5 @@
|
||||
export {default as PluginDetails} from './PluginDetails';
|
||||
export {default as getPluginDetails} from './getPluginDetails';
|
||||
export * from './pluginInstaller';
|
||||
export * from './getInstalledPlugins';
|
||||
export * from './getUpdatablePlugins';
|
||||
|
||||
@@ -23,8 +23,6 @@ import {
|
||||
} from './pluginPaths';
|
||||
import semver from 'semver';
|
||||
|
||||
export type PluginMap = Map<string, PluginDetails>;
|
||||
|
||||
const getTmpDir = promisify(tmp.dir) as () => Promise<string>;
|
||||
|
||||
function providePluginManager(): PM {
|
||||
@@ -51,7 +49,7 @@ function getPluginPendingInstallationsDir(name: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
function getPluginInstallationDir(name: string): string {
|
||||
export function getPluginInstallationDir(name: string): string {
|
||||
return path.join(
|
||||
pluginInstallationDir,
|
||||
replaceInvalidPathSegmentCharacters(name),
|
||||
@@ -183,83 +181,11 @@ export async function installPluginFromFile(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInstalledPlugins(): Promise<PluginMap> {
|
||||
const pluginDirExists = await fs.pathExists(pluginInstallationDir);
|
||||
if (!pluginDirExists) {
|
||||
return new Map();
|
||||
}
|
||||
const dirs = await fs.readdir(pluginInstallationDir);
|
||||
const plugins = await Promise.all<[string, PluginDetails]>(
|
||||
dirs.map(
|
||||
(dirName) =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
const pluginDir = path.join(pluginInstallationDir, dirName);
|
||||
if (!(await fs.lstat(pluginDir)).isDirectory()) {
|
||||
return resolve(undefined);
|
||||
}
|
||||
try {
|
||||
const details = await getPluginDetails(pluginDir);
|
||||
resolve([details.name, details]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
return new Map(plugins.filter(Boolean));
|
||||
}
|
||||
|
||||
export async function getPendingInstallationPlugins(): Promise<PluginMap> {
|
||||
const pluginDirExists = await fs.pathExists(pluginPendingInstallationDir);
|
||||
if (!pluginDirExists) {
|
||||
return new Map();
|
||||
}
|
||||
const dirs = await fs.readdir(pluginPendingInstallationDir);
|
||||
const plugins = await Promise.all<[string, PluginDetails]>(
|
||||
dirs.map(
|
||||
(dirName) =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
const versions = (
|
||||
await fs.readdir(path.join(pluginPendingInstallationDir, dirName))
|
||||
).sort((v1, v2) => semver.compare(v2, v1, true));
|
||||
if (versions.length === 0) {
|
||||
return resolve(undefined);
|
||||
}
|
||||
const pluginDir = path.join(
|
||||
pluginPendingInstallationDir,
|
||||
dirName,
|
||||
versions[0],
|
||||
);
|
||||
if (!(await fs.lstat(pluginDir)).isDirectory()) {
|
||||
return resolve(undefined);
|
||||
}
|
||||
try {
|
||||
const details = await getPluginDetails(pluginDir);
|
||||
resolve([details.name, details]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
return new Map(plugins.filter(Boolean));
|
||||
}
|
||||
|
||||
export async function getPendingAndInstalledPlugins(): Promise<PluginMap> {
|
||||
const plugins = await getInstalledPlugins();
|
||||
for (const [name, details] of await getPendingInstallationPlugins()) {
|
||||
if (
|
||||
!plugins.get(name) ||
|
||||
semver.gt(details.version, plugins.get(name)!.version)
|
||||
) {
|
||||
plugins.set(name, details);
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
export async function removePlugin(name: string): Promise<void> {
|
||||
await fs.remove(getPluginInstallationDir(name));
|
||||
await Promise.all([
|
||||
fs.remove(getPluginInstallationDir(name)),
|
||||
fs.remove(getPluginPendingInstallationsDir(name)),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function finishPendingPluginInstallations() {
|
||||
|
||||
15
desktop/plugin-lib/src/typeUtils.ts
Normal file
15
desktop/plugin-lib/src/typeUtils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
// TODO T75614643: move to a separate lib for utils, e.g. flipper-utils
|
||||
// Typescript doesn't know Array.filter(Boolean) won't contain nulls.
|
||||
// So use Array.filter(notNull) instead.
|
||||
export function notNull<T>(x: T | null | undefined): x is T {
|
||||
return x !== null && x !== undefined;
|
||||
}
|
||||
1
desktop/types/npm-api.d.ts
vendored
1
desktop/types/npm-api.d.ts
vendored
@@ -32,5 +32,6 @@ declare module 'npm-api' {
|
||||
export interface Package {
|
||||
name: string;
|
||||
version: string;
|
||||
[name: string]: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,150 +12,109 @@
|
||||
resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f"
|
||||
integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA==
|
||||
|
||||
"@algolia/cache-browser-local-storage@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.1.0.tgz#c4f1bfc57ea562248072b35831e3c4b646cc3921"
|
||||
integrity sha512-r8BOgqZXVt+JPgP19PQNzZ+lYP+MP6eZKNQqfRYofFEx+K9oyfdtGCqmoWJsBUi3nNOzhbOcg2jfP2GJzJBZ5g==
|
||||
"@algolia/cache-browser-local-storage@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.4.0.tgz#f58055bdf798d7b31b6d5f86e465cb0fc7dd6694"
|
||||
integrity sha512-2AiKgN7DpFypkRCRkpqH7waXXyFdcnsPWzmN8sLHrB/FfXqgmsQb3pGft+9YHZIDQ0vAnfgMxSGgMhMGW+0Qnw==
|
||||
dependencies:
|
||||
"@algolia/cache-common" "4.1.0"
|
||||
"@algolia/cache-common" "4.4.0"
|
||||
|
||||
"@algolia/cache-common@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.1.0.tgz#ab895f8049ff7064ca1bfea504a56f97fd5d4683"
|
||||
integrity sha512-ZvvK40bs1BWLErchleZL4ctHT2uH56uLMnpZPCuIk+H2PKddeiIQc/z2JDu2BHr68u513XIAAoQ+C+LgKNugmw==
|
||||
"@algolia/cache-common@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.4.0.tgz#bfe84790230f5d2de495238b29e9397c5ed2b26e"
|
||||
integrity sha512-PrIgoMnXaDWUfwOekahro543pgcJfgRu/nd/ZQS5ffem3+Ow725eZY6HDpPaQ1k3cvLii9JH6V2sNJConjqUKA==
|
||||
|
||||
"@algolia/cache-common@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.3.0.tgz#3a257b184bce678e524e354c4f4abd3235ccd24d"
|
||||
integrity sha512-AHTbOn9lk0f5IkjssXXmDgnaZfsUJVZ61sqOH1W3LyJdAscDzCj0KtwijELn8FHlLXQak7+K93/O3Oct0uHncQ==
|
||||
|
||||
"@algolia/cache-in-memory@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.1.0.tgz#cb9b575df1ebe3befd198a50a444a7d181e50853"
|
||||
integrity sha512-2382OXYFDeoPLA5vP9KP58ad15ows24ML5/io/T1N0xsZ0eVXDkT52qgaJw/esUfEkWScZ2R8kpesUa+qEP+kw==
|
||||
"@algolia/cache-in-memory@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.4.0.tgz#54a089094c2afa5b9cacab4b60a5f1ba29013a7c"
|
||||
integrity sha512-9+XlUB0baDU/Dp9URRHPp6Q37YmTO0QmgPWt9+n+wqZrRL0jR3Jezr4jCT7RemqGMxBiR+YpnqaUv0orpb0ptw==
|
||||
dependencies:
|
||||
"@algolia/cache-common" "4.1.0"
|
||||
"@algolia/cache-common" "4.4.0"
|
||||
|
||||
"@algolia/client-account@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.1.0.tgz#a31d26c22e6a56554ea4aa8552d153b1a1aa4363"
|
||||
integrity sha512-GFINlsxAHM/GEeDBjoTx8+J1ra9SINQCuXi2C9QSLFClPKug2lzApm8niJJGXckhyZ2aDLb7drJ1qJ8bTspApw==
|
||||
"@algolia/client-account@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.4.0.tgz#7dbeff83e1c85d853b3ad224674a924e02b94d1b"
|
||||
integrity sha512-Kynu3cMEs0clTLf674rtrCF+FWR/JwlQxKlIWsPzvLBRmNXdvYej9YBcNaOr4OTQFCCZn9JVE8ib91Z7J4IL1Q==
|
||||
dependencies:
|
||||
"@algolia/client-common" "4.1.0"
|
||||
"@algolia/client-search" "4.1.0"
|
||||
"@algolia/transporter" "4.1.0"
|
||||
"@algolia/client-common" "4.4.0"
|
||||
"@algolia/client-search" "4.4.0"
|
||||
"@algolia/transporter" "4.4.0"
|
||||
|
||||
"@algolia/client-analytics@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.1.0.tgz#eb05ccb636351b2d6494b2affb6034b791236998"
|
||||
integrity sha512-JMyZ9vXGbTJWiO66fWEu9uJ7GSYfouUyaq8W/6esADPtBbelf+Nc0NRlicOwHHJGwiJvWdvELafxrhkR1+KR8A==
|
||||
"@algolia/client-analytics@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.4.0.tgz#50dde68b067c615fc91434c98db9b5ca429be33d"
|
||||
integrity sha512-GQyjQimKAc9sZbafxln9Wk7j4pEYiORv28MZkZ+0Bjt7WNXIeO7OgOOECVpQHm9buyV6hCKpNtJcbb5/syRzdQ==
|
||||
dependencies:
|
||||
"@algolia/client-common" "4.1.0"
|
||||
"@algolia/client-search" "4.1.0"
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
"@algolia/transporter" "4.1.0"
|
||||
"@algolia/client-common" "4.4.0"
|
||||
"@algolia/client-search" "4.4.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
"@algolia/transporter" "4.4.0"
|
||||
|
||||
"@algolia/client-common@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.1.0.tgz#cd3a71cef1e0d87476252cbee20b0da938f6221c"
|
||||
integrity sha512-fjSMKeG54vAyQAhf+uz039/birTiLun8nDuCNx4CUbzGl97M0g96Q8jpsiZa0cjSNgh0VakMzn2GnHbS55W9/Q==
|
||||
"@algolia/client-common@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.4.0.tgz#b9fa987bc7a148f9756da59ada51fe2494a4aa9a"
|
||||
integrity sha512-a3yr6UhzjWPHDG/8iGp9UvrDOm1aeHVWJIf0Nj/cIvqX5tNCEIo4IMe59ovApkDgLOIpt/cLsyhn9/FiPXRhJA==
|
||||
dependencies:
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
"@algolia/transporter" "4.1.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
"@algolia/transporter" "4.4.0"
|
||||
|
||||
"@algolia/client-common@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.3.0.tgz#d386f67a8068e5ca2d2a00d37fab10a653744951"
|
||||
integrity sha512-8Ohj6zXZkpwDKc8ZWVTZo2wPO4+LT5D258suGg/C6nh4UxOrFOp6QaqeQo8JZ1eqMqtfb3zv5SHgW4fZ00NCLQ==
|
||||
"@algolia/client-recommendation@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-recommendation/-/client-recommendation-4.4.0.tgz#82410f7a346ed8518b8dcd28bc47571e850ab74f"
|
||||
integrity sha512-sBszbQH46rko6w2fdEG77ma8+fAg0SDkLZGxWhv4trgcnYGUBFl2dcpEPt/6koto9b4XYlf+eh+qi6iGvYqRPg==
|
||||
dependencies:
|
||||
"@algolia/requester-common" "4.3.0"
|
||||
"@algolia/transporter" "4.3.0"
|
||||
"@algolia/client-common" "4.4.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
"@algolia/transporter" "4.4.0"
|
||||
|
||||
"@algolia/client-recommendation@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-recommendation/-/client-recommendation-4.1.0.tgz#a0a26de4a6dd902d7ca55cf381cce3a7280d5b49"
|
||||
integrity sha512-UEN/QgQwVtVH++yAs2uTuyZZQQ1p5Xs/7/FKT4Kh9/8NAyqDD49zuyq/giw8PRNhWc3C/9jiO7X4RKE8QrVWGw==
|
||||
"@algolia/client-search@4.4.0", "@algolia/client-search@^4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.4.0.tgz#c1e107206f3ae719cd3a9877889eea5e5cbcdc62"
|
||||
integrity sha512-jqWcxCUyPPHnHreoMb2PnN9iHTP+V/nL62R84XuTRDE3VgTnhm4ZnqyuRdzZQqaz+gNy5znav64TmQ9FN9WW5g==
|
||||
dependencies:
|
||||
"@algolia/client-common" "4.1.0"
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
"@algolia/transporter" "4.1.0"
|
||||
"@algolia/client-common" "4.4.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
"@algolia/transporter" "4.4.0"
|
||||
|
||||
"@algolia/client-search@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.1.0.tgz#07cc422af997e409968d3b74142e984aa71ae38c"
|
||||
integrity sha512-bpCYMEXUdyiopEBSHHwnrRhNEwOLstIeb0Djz+/pVuTXEr3Xg3JUoAZ8xFsCVldcXaZQpbi1/T0y3ky6xUVzfw==
|
||||
"@algolia/logger-common@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.4.0.tgz#8115d95d5f6227f0127d33130a9c4622cde64f6f"
|
||||
integrity sha512-2vjmSENLaKNuF+ytRDysfWxxgFG95WXCHwHbueThdPMCK3hskkwqJ0Y/pugKfzl+54mZxegb4BYfgcCeuaHVUw==
|
||||
|
||||
"@algolia/logger-console@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.4.0.tgz#1e0eaaf0879f152f9a1fa333c4cd8cb55e071552"
|
||||
integrity sha512-st/GUWyKvr6YM72OOfF+RmpdVGda3BPXbQ+chpntUq1WyVkyZXGjSmH1IcBVlua27GzxabwOUYON39cF3x10/g==
|
||||
dependencies:
|
||||
"@algolia/client-common" "4.1.0"
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
"@algolia/transporter" "4.1.0"
|
||||
"@algolia/logger-common" "4.4.0"
|
||||
|
||||
"@algolia/client-search@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.3.0.tgz#9f28df3b97d0b26605b9d6c5e69ea0df39e81c53"
|
||||
integrity sha512-KCgcIsNMW1/0F5OILiFTddbTAKduJHRvXQS4NxY1H9gQWMTVeWJS7VZQ/ukKBiUMLatwUQHJz2qpYm9fmqOjkQ==
|
||||
"@algolia/requester-browser-xhr@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.4.0.tgz#f5877397ed92d2d64d08846ea969aeb559a5efb6"
|
||||
integrity sha512-V3a4hXlNch355GnWaT1f5QfXhROpsjT6sd0Znq29gAhwLqfBExhLW6Khdkv5pENC0Qy7ClVhdXFrBL9QCQer1g==
|
||||
dependencies:
|
||||
"@algolia/client-common" "4.3.0"
|
||||
"@algolia/requester-common" "4.3.0"
|
||||
"@algolia/transporter" "4.3.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
|
||||
"@algolia/logger-common@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.1.0.tgz#05608dee38dfa35bfe37874683760140d471bfdc"
|
||||
integrity sha512-QrE4Srf1LB7ekLzl68bFqlTrv7Wk7+GpsaGfB4xFZ9Pfv89My9p7qTVqdLlA44hEFY3fZ9csJp1/PFVucgNB4w==
|
||||
"@algolia/requester-common@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.4.0.tgz#0e977939aae32ff81a6d27480a71771a65db6051"
|
||||
integrity sha512-jPinHlFJEFokxQ5b3JWyjQKKn+FMy0hH99PApzOgQAYOSiFRXiPEZp6LeIexDeLLu7Y3eRt/3nHvjPKa6PmRRw==
|
||||
|
||||
"@algolia/logger-common@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.3.0.tgz#ab01dd0458f9e5c1dd8e9ea43d604d7e4b76ad33"
|
||||
integrity sha512-vQ+aukjZkRAyO9iyINBefT366UtF/B9QoA1Kw8PlY67T6fYmklFgYp3LNH/e7h/gz0py5LYY/HIwSsaTKk8/VQ==
|
||||
|
||||
"@algolia/logger-console@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.1.0.tgz#099ee86716aea4c976345417397ddfa1338a5acc"
|
||||
integrity sha512-sKELkiKIrj/tPRAdhOPNI0UxhK2uiIUXnGs/3ztAif6QX7vyE3lY19sj5pIVJctRvl8LW2UlzpBFGlcCDkho9Q==
|
||||
"@algolia/requester-node-http@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.4.0.tgz#6ffba93d54eeadf64cb1be67fae5c4e3f7c8f390"
|
||||
integrity sha512-b7HC9C/GHxiV4+0GpCRTtjscvwarPr3dGm4CAhb6AkNjgjRcFUNr1NfsF75w3WVmzmt79/7QZihddztDdVMGjw==
|
||||
dependencies:
|
||||
"@algolia/logger-common" "4.1.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
|
||||
"@algolia/requester-browser-xhr@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.1.0.tgz#a7ab63f184f3d0aa8e85ac73ce39c528271c6d9b"
|
||||
integrity sha512-bLMfIAkOLs1/vGA09yxU0N5+bE0fSSvEH2ySqVssfWLMP+KRAvby2Goxm8BgI9xLkOvLbhazfQ4Ov2448VvA1g==
|
||||
"@algolia/transporter@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.4.0.tgz#6ec79aac43bc515c8e4f6d6e27dc8d8cd7112f7e"
|
||||
integrity sha512-Xxzq91DEEeKIzT3DU46n4LEyTGAKZNtSHc2H9wvIY5MYwhZwEribmXXZ6k8W1FvBvzggv3juu0SP+xwGoR7F0w==
|
||||
dependencies:
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
|
||||
"@algolia/requester-common@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.1.0.tgz#91907e9963e455b11862d1cca02fc1d1d961dbce"
|
||||
integrity sha512-Cy0ciOv5uIm6wF+uLc9DHhxgPJtYQuy1f//hwJcW5mlPX/prPgxWwLXzWyyA+Ca7uU3q+0Y3cIFvEWM5pDxMEg==
|
||||
|
||||
"@algolia/requester-common@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.3.0.tgz#1529e51082a9b43d324290f3c07b6acb7cc34cd8"
|
||||
integrity sha512-1v73KyspJBiTzfyXupjHxikxTYjh5MoxI6mOIvAtQxRqc4ehUPAEdPCNHEvvLiCK96iKWzZaULmV0U7pj3yvTw==
|
||||
|
||||
"@algolia/requester-node-http@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.1.0.tgz#db0a224538691f6fab18ced27c548cf3b4017689"
|
||||
integrity sha512-tXp6Pjx9dFgM5ccW6YfEN6v2Zqq8uGwhS1pyq03/aRYRBK60LptjG5jo++vrOytrQDOnIjcZtQzBQch2GjCVmw==
|
||||
dependencies:
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
|
||||
"@algolia/transporter@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.1.0.tgz#18cb8837ca4079a23572a3b7dbefece71fb6fff3"
|
||||
integrity sha512-Z7PjHazSC+KFLDuCFOjvRNgLfh7XOE4tXi0a9O3gBRup4Sk3VQCfTw4ygCF3rRx6uYbq192efLu0nL1E9azxLA==
|
||||
dependencies:
|
||||
"@algolia/cache-common" "4.1.0"
|
||||
"@algolia/logger-common" "4.1.0"
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
|
||||
"@algolia/transporter@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.3.0.tgz#17dcafcd20bb30d2bef8886c34e86c5d47e1c560"
|
||||
integrity sha512-BTKHAtdQdfOJ0xzZkiyEK/2QVQJTiVgBZlOBfXp2gBtztjV26OqfW4n6Xz0o7eBRzLEwY1ot3mHF5QIVUjAsMg==
|
||||
dependencies:
|
||||
"@algolia/cache-common" "4.3.0"
|
||||
"@algolia/logger-common" "4.3.0"
|
||||
"@algolia/requester-common" "4.3.0"
|
||||
"@algolia/cache-common" "4.4.0"
|
||||
"@algolia/logger-common" "4.4.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
|
||||
version "7.10.4"
|
||||
@@ -2787,25 +2746,25 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.5.5:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
algoliasearch@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.1.0.tgz#d422ac0d115497021a6c96f4b9747dbaa63f164a"
|
||||
integrity sha512-0lzjvqQZkJYPuv7LyQauMIMCFFzJWfUf3m9KuHjmFubwbnTDa87KCMXKouMJ0kWXXt6nTLNt0+2YRREOWx2PHw==
|
||||
algoliasearch@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.4.0.tgz#25c356d8bdcf7e3f941633f61e1ac111ddcba404"
|
||||
integrity sha512-Ag3wxe/nSodNl/1KbHibtkh7TNLptKE300/wnGVtszRjXivaWD6333nUpCumrYObHym/fHMHyLcmQYezXbAIWQ==
|
||||
dependencies:
|
||||
"@algolia/cache-browser-local-storage" "4.1.0"
|
||||
"@algolia/cache-common" "4.1.0"
|
||||
"@algolia/cache-in-memory" "4.1.0"
|
||||
"@algolia/client-account" "4.1.0"
|
||||
"@algolia/client-analytics" "4.1.0"
|
||||
"@algolia/client-common" "4.1.0"
|
||||
"@algolia/client-recommendation" "4.1.0"
|
||||
"@algolia/client-search" "4.1.0"
|
||||
"@algolia/logger-common" "4.1.0"
|
||||
"@algolia/logger-console" "4.1.0"
|
||||
"@algolia/requester-browser-xhr" "4.1.0"
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
"@algolia/requester-node-http" "4.1.0"
|
||||
"@algolia/transporter" "4.1.0"
|
||||
"@algolia/cache-browser-local-storage" "4.4.0"
|
||||
"@algolia/cache-common" "4.4.0"
|
||||
"@algolia/cache-in-memory" "4.4.0"
|
||||
"@algolia/client-account" "4.4.0"
|
||||
"@algolia/client-analytics" "4.4.0"
|
||||
"@algolia/client-common" "4.4.0"
|
||||
"@algolia/client-recommendation" "4.4.0"
|
||||
"@algolia/client-search" "4.4.0"
|
||||
"@algolia/logger-common" "4.4.0"
|
||||
"@algolia/logger-console" "4.4.0"
|
||||
"@algolia/requester-browser-xhr" "4.4.0"
|
||||
"@algolia/requester-common" "4.4.0"
|
||||
"@algolia/requester-node-http" "4.4.0"
|
||||
"@algolia/transporter" "4.4.0"
|
||||
|
||||
ansi-align@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
||||
Reference in New Issue
Block a user