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,
|
"privileged": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@algolia/client-search": "4.3.0",
|
|
||||||
"@emotion/core": "^10.0.22",
|
"@emotion/core": "^10.0.22",
|
||||||
"@emotion/styled": "^10.0.23",
|
"@emotion/styled": "^10.0.23",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
"JSONStream": "^1.3.1",
|
"JSONStream": "^1.3.1",
|
||||||
"adbkit": "^2.11.1",
|
"adbkit": "^2.11.1",
|
||||||
"adbkit-logcat": "^2.0.1",
|
"adbkit-logcat": "^2.0.1",
|
||||||
"algoliasearch": "^4.0.0",
|
|
||||||
"archiver": "^5.0.0",
|
"archiver": "^5.0.0",
|
||||||
"async-mutex": "^0.1.3",
|
"async-mutex": "^0.1.3",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
@@ -37,7 +35,6 @@
|
|||||||
"invariant": "^2.2.2",
|
"invariant": "^2.2.2",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"lodash.memoize": "^4.1.2",
|
"lodash.memoize": "^4.1.2",
|
||||||
"npm-api": "^1.0.0",
|
|
||||||
"open": "^7.0.0",
|
"open": "^7.0.0",
|
||||||
"openssl-wrapper": "^0.3.4",
|
"openssl-wrapper": "^0.3.4",
|
||||||
"promise-retry": "^1.1.1",
|
"promise-retry": "^1.1.1",
|
||||||
|
|||||||
@@ -25,24 +25,19 @@ import {
|
|||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import React, {useCallback, useState, useMemo, useEffect} from 'react';
|
import React, {useCallback, useState, useEffect} from 'react';
|
||||||
import {List} from 'immutable';
|
import {List} from 'immutable';
|
||||||
import {SearchIndex} from 'algoliasearch';
|
|
||||||
import {SearchResponse} from '@algolia/client-search';
|
|
||||||
import {reportPlatformFailures, reportUsage} from '../../utils/metrics';
|
import {reportPlatformFailures, reportUsage} from '../../utils/metrics';
|
||||||
import restartFlipper from '../../utils/restartFlipper';
|
import restartFlipper from '../../utils/restartFlipper';
|
||||||
import {registerInstalledPlugins} from '../../reducers/pluginManager';
|
import {registerInstalledPlugins} from '../../reducers/pluginManager';
|
||||||
import {
|
import {
|
||||||
getPendingAndInstalledPlugins,
|
|
||||||
removePlugin,
|
|
||||||
PluginMap,
|
|
||||||
PluginDetails,
|
|
||||||
} from 'flipper-plugin-lib';
|
|
||||||
import {
|
|
||||||
provideSearchIndex,
|
|
||||||
findPluginUpdates as _findPluginUpdates,
|
|
||||||
UpdateResult,
|
UpdateResult,
|
||||||
} from '../../utils/pluginManager';
|
getInstalledPlugins,
|
||||||
|
getUpdatablePlugins,
|
||||||
|
removePlugin,
|
||||||
|
UpdatablePluginDetails,
|
||||||
|
InstalledPluginDetails,
|
||||||
|
} from 'flipper-plugin-lib';
|
||||||
import {installPluginFromNpm} from 'flipper-plugin-lib';
|
import {installPluginFromNpm} from 'flipper-plugin-lib';
|
||||||
import {State as AppState} from '../../reducers';
|
import {State as AppState} from '../../reducers';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
@@ -97,7 +92,7 @@ const RestartBar = styled(FlexColumn)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type PropsFromState = {
|
type PropsFromState = {
|
||||||
installedPlugins: PluginMap;
|
installedPlugins: InstalledPluginDetails[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchFromProps = {
|
type DispatchFromProps = {
|
||||||
@@ -105,58 +100,29 @@ type DispatchFromProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
searchIndexFactory: () => SearchIndex;
|
|
||||||
autoHeight: boolean;
|
autoHeight: boolean;
|
||||||
findPluginUpdates: (
|
|
||||||
currentPlugins: PluginMap,
|
|
||||||
) => Promise<[string, UpdateResult][]>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = OwnProps & PropsFromState & DispatchFromProps;
|
type Props = OwnProps & PropsFromState & DispatchFromProps;
|
||||||
|
|
||||||
const defaultProps: OwnProps = {
|
const defaultProps: OwnProps = {
|
||||||
searchIndexFactory: provideSearchIndex,
|
|
||||||
autoHeight: false,
|
autoHeight: false,
|
||||||
findPluginUpdates: _findPluginUpdates,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpdatablePlugin = {
|
const PluginInstaller = function ({
|
||||||
updateStatus: UpdateResult;
|
refreshInstalledPlugins,
|
||||||
};
|
installedPlugins,
|
||||||
|
autoHeight,
|
||||||
type UpdatablePluginDefinition = PluginDetails & UpdatablePlugin;
|
}: Props) {
|
||||||
|
|
||||||
// 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 [restartRequired, setRestartRequired] = useState(false);
|
const [restartRequired, setRestartRequired] = useState(false);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
const onInstall = useCallback(async () => {
|
const onInstall = useCallback(async () => {
|
||||||
props.refreshInstalledPlugins();
|
refreshInstalledPlugins();
|
||||||
setRestartRequired(true);
|
setRestartRequired(true);
|
||||||
}, []);
|
}, [refreshInstalledPlugins]);
|
||||||
|
|
||||||
const rows = useNPMSearch(
|
const rows = useNPMSearch(query, onInstall, installedPlugins);
|
||||||
query,
|
|
||||||
setQuery,
|
|
||||||
props.searchIndexFactory,
|
|
||||||
props.installedPlugins,
|
|
||||||
onInstall,
|
|
||||||
props.findPluginUpdates,
|
|
||||||
);
|
|
||||||
const restartApp = useCallback(() => {
|
const restartApp = useCallback(() => {
|
||||||
restartFlipper();
|
restartFlipper();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -187,7 +153,7 @@ const PluginInstaller = function (props: Props) {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
highlightableRows={false}
|
highlightableRows={false}
|
||||||
highlightedRows={new Set()}
|
highlightedRows={new Set()}
|
||||||
autoHeight={props.autoHeight}
|
autoHeight={autoHeight}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -195,7 +161,6 @@ const PluginInstaller = function (props: Props) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
PluginInstaller.defaultProps = defaultProps;
|
|
||||||
|
|
||||||
const TableButton = styled(Button)({
|
const TableButton = styled(Button)({
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
@@ -209,18 +174,10 @@ const AlignedGlyph = styled(Glyph)({
|
|||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
});
|
});
|
||||||
|
|
||||||
function liftUpdatable(val: PluginDetails): UpdatablePluginDefinition {
|
|
||||||
return {
|
|
||||||
...val,
|
|
||||||
updateStatus: {kind: 'up-to-date'},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstallButton(props: {
|
function InstallButton(props: {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
onInstall: () => void;
|
onInstall: () => void;
|
||||||
installed: boolean;
|
|
||||||
updateStatus: UpdateResult;
|
updateStatus: UpdateResult;
|
||||||
}) {
|
}) {
|
||||||
type InstallAction =
|
type InstallAction =
|
||||||
@@ -280,9 +237,9 @@ function InstallButton(props: {
|
|||||||
const [action, setAction] = useState<InstallAction>(
|
const [action, setAction] = useState<InstallAction>(
|
||||||
props.updateStatus.kind === 'update-available'
|
props.updateStatus.kind === 'update-available'
|
||||||
? {kind: 'Update'}
|
? {kind: 'Update'}
|
||||||
: props.installed
|
: props.updateStatus.kind === 'not-installed'
|
||||||
? {kind: 'Remove'}
|
? {kind: 'Install'}
|
||||||
: {kind: 'Install'},
|
: {kind: 'Remove'},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (action.kind === 'Waiting') {
|
if (action.kind === 'Waiting') {
|
||||||
@@ -332,22 +289,19 @@ function InstallButton(props: {
|
|||||||
|
|
||||||
function useNPMSearch(
|
function useNPMSearch(
|
||||||
query: string,
|
query: string,
|
||||||
setQuery: (query: string) => void,
|
onInstall: () => void,
|
||||||
searchClientFactory: () => SearchIndex,
|
installedPlugins: InstalledPluginDetails[],
|
||||||
installedPlugins: PluginMap,
|
|
||||||
onInstall: () => Promise<void>,
|
|
||||||
findPluginUpdates: (
|
|
||||||
currentPlugins: PluginMap,
|
|
||||||
) => Promise<[string, UpdateResult][]>,
|
|
||||||
): TableRows_immutable {
|
): TableRows_immutable {
|
||||||
const index = useMemo(searchClientFactory, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reportUsage(`${TAG}:open`);
|
reportUsage(`${TAG}:open`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [searchResults, setSearchResults] = useState<UpdatablePluginDetails[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const createRow = useCallback(
|
const createRow = useCallback(
|
||||||
(h: UpdatablePluginDefinition) => ({
|
(h: UpdatablePluginDetails) => ({
|
||||||
key: h.name,
|
key: h.name,
|
||||||
columns: {
|
columns: {
|
||||||
name: {
|
name: {
|
||||||
@@ -378,7 +332,6 @@ function useNPMSearch(
|
|||||||
name={h.name}
|
name={h.name}
|
||||||
version={h.version}
|
version={h.version}
|
||||||
onInstall={onInstall}
|
onInstall={onInstall}
|
||||||
installed={installedPlugins.has(h.name)}
|
|
||||||
updateStatus={h.updateStatus}
|
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(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const {hits} = await reportPlatformFailures(
|
const updatablePlugins = await reportPlatformFailures(
|
||||||
index.search<PluginDetails>('', {
|
getUpdatablePlugins(),
|
||||||
query,
|
|
||||||
filters: 'keywords:flipper-plugin',
|
|
||||||
hitsPerPage: 20,
|
|
||||||
}) as Promise<SearchResponse<PluginDetails>>,
|
|
||||||
`${TAG}:queryIndex`,
|
`${TAG}:queryIndex`,
|
||||||
);
|
);
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSearchResults(
|
setSearchResults(updatablePlugins);
|
||||||
hits
|
|
||||||
.filter((hit) => !installedPlugins.has(hit.name))
|
|
||||||
.map(liftUpdatable),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up: if query changes while we're searching, abandon results.
|
// Clean up: if query changes while we're searching, abandon results.
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -424,28 +360,19 @@ function useNPMSearch(
|
|||||||
})();
|
})();
|
||||||
}, [query, installedPlugins]);
|
}, [query, installedPlugins]);
|
||||||
|
|
||||||
useEffect(() => {
|
const rows: TableRows_immutable = List(searchResults.map(createRow));
|
||||||
(async () => {
|
return rows;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PluginInstaller.defaultProps = defaultProps;
|
||||||
|
|
||||||
export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
|
export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
|
||||||
({pluginManager: {installedPlugins}}) => ({
|
({pluginManager: {installedPlugins}}) => ({
|
||||||
installedPlugins,
|
installedPlugins,
|
||||||
}),
|
}),
|
||||||
(dispatch: Dispatch<Action<any>>) => ({
|
(dispatch: Dispatch<Action<any>>) => ({
|
||||||
refreshInstalledPlugins: () => {
|
refreshInstalledPlugins: () => {
|
||||||
getPendingAndInstalledPlugins().then((plugins) =>
|
getInstalledPlugins().then((plugins) =>
|
||||||
dispatch(registerInstalledPlugins(plugins)),
|
dispatch(registerInstalledPlugins(plugins)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,84 +7,109 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {annotatePluginsWithUpdates} from '../PluginInstaller';
|
jest.mock('flipper-plugin-lib');
|
||||||
import {UpdateResult} from '../../../utils/pluginManager';
|
|
||||||
import {PluginDetails} from 'flipper-plugin-lib';
|
|
||||||
|
|
||||||
test('annotatePluginsWithUpdates', async () => {
|
import {default as PluginInstaller} from '../PluginInstaller';
|
||||||
const installedPlugins = new Map<string, PluginDetails>([
|
import React from 'react';
|
||||||
[
|
import {render, waitForElement} from '@testing-library/react';
|
||||||
'example',
|
import configureStore from 'redux-mock-store';
|
||||||
{
|
import {Provider} from 'react-redux';
|
||||||
name: 'example',
|
import type {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||||
version: '0.1.0',
|
import {getUpdatablePlugins, UpdatablePluginDetails} from 'flipper-plugin-lib';
|
||||||
description: 'Gaze into the death crystal',
|
import {Store} from '../../../reducers';
|
||||||
dir: '/plugins/example',
|
import {mocked} from 'ts-jest/utils';
|
||||||
specVersion: 2,
|
|
||||||
source: 'src/index.ts',
|
const getUpdatablePluginsMock = mocked(getUpdatablePlugins);
|
||||||
isDefault: false,
|
|
||||||
main: 'lib/index.js',
|
function getStore(installedPlugins: InstalledPluginDetails[] = []): Store {
|
||||||
title: 'Example',
|
return configureStore([])({
|
||||||
id: 'Example',
|
application: {sessionId: 'mysession'},
|
||||||
entry: '/plugins/example/lib/index.js',
|
pluginManager: {installedPlugins},
|
||||||
},
|
}) as Store;
|
||||||
],
|
}
|
||||||
[
|
|
||||||
'ricksybusiness',
|
const samplePluginDetails1: UpdatablePluginDetails = {
|
||||||
{
|
name: 'flipper-plugin-hello',
|
||||||
name: 'ricksybusiness',
|
entry: './test/index.js',
|
||||||
version: '1.0.0',
|
version: '0.1.0',
|
||||||
description: 'Rick Die Rickpeat',
|
specVersion: 2,
|
||||||
dir: '/plugins/example',
|
main: 'dist/bundle.js',
|
||||||
specVersion: 2,
|
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample1',
|
||||||
source: 'src/index.ts',
|
source: 'src/index.js',
|
||||||
isDefault: false,
|
id: 'Hello',
|
||||||
main: 'lib/index.js',
|
title: 'Hello',
|
||||||
title: 'ricksybusiness',
|
description: 'World?',
|
||||||
id: 'ricksybusiness',
|
isDefault: false,
|
||||||
entry: '/plugins/ricksybusiness/lib/index.js',
|
updateStatus: {
|
||||||
},
|
kind: 'not-installed',
|
||||||
],
|
version: '0.1.0',
|
||||||
]);
|
},
|
||||||
const updates = new Map<string, UpdateResult>([
|
};
|
||||||
['example', {kind: 'update-available', version: '1.1.0'}],
|
|
||||||
]);
|
const samplePluginDetails2: UpdatablePluginDetails = {
|
||||||
const res = annotatePluginsWithUpdates(installedPlugins, updates);
|
name: 'flipper-plugin-world',
|
||||||
expect(res).toMatchInlineSnapshot(`
|
entry: './test/index.js',
|
||||||
Map {
|
version: '0.2.0',
|
||||||
"example" => Object {
|
specVersion: 2,
|
||||||
"description": "Gaze into the death crystal",
|
main: 'dist/bundle.js',
|
||||||
"dir": "/plugins/example",
|
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample2',
|
||||||
"entry": "/plugins/example/lib/index.js",
|
source: 'src/index.js',
|
||||||
"id": "Example",
|
id: 'World',
|
||||||
"isDefault": false,
|
title: 'World',
|
||||||
"main": "lib/index.js",
|
description: 'Hello?',
|
||||||
"name": "example",
|
isDefault: false,
|
||||||
"source": "src/index.ts",
|
updateStatus: {
|
||||||
"specVersion": 2,
|
kind: 'not-installed',
|
||||||
"title": "Example",
|
version: '0.2.0',
|
||||||
"updateStatus": Object {
|
},
|
||||||
"kind": "update-available",
|
};
|
||||||
"version": "1.1.0",
|
|
||||||
},
|
const SEARCH_RESULTS = [samplePluginDetails1, samplePluginDetails2];
|
||||||
"version": "0.1.0",
|
|
||||||
},
|
afterEach(() => {
|
||||||
"ricksybusiness" => Object {
|
getUpdatablePluginsMock.mockClear();
|
||||||
"description": "Rick Die Rickpeat",
|
});
|
||||||
"dir": "/plugins/example",
|
|
||||||
"entry": "/plugins/ricksybusiness/lib/index.js",
|
test('load PluginInstaller list', async () => {
|
||||||
"id": "ricksybusiness",
|
getUpdatablePluginsMock.mockReturnValue(Promise.resolve(SEARCH_RESULTS));
|
||||||
"isDefault": false,
|
const component = (
|
||||||
"main": "lib/index.js",
|
<Provider store={getStore()}>
|
||||||
"name": "ricksybusiness",
|
<PluginInstaller
|
||||||
"source": "src/index.ts",
|
// Bit ugly to have this as an effectively test-only option, but
|
||||||
"specVersion": 2,
|
// without, we rely on height information from Electron which we don't
|
||||||
"title": "ricksybusiness",
|
// have, causing no items to be rendered.
|
||||||
"updateStatus": Object {
|
autoHeight={true}
|
||||||
"kind": "up-to-date",
|
/>
|
||||||
},
|
</Provider>
|
||||||
"version": "1.0.0",
|
);
|
||||||
},
|
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 {Store} from '../reducers/index';
|
||||||
import {Logger} from '../fb-interfaces/Logger';
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
||||||
import {getPendingAndInstalledPlugins} from 'flipper-plugin-lib';
|
import {getInstalledPlugins} from 'flipper-plugin-lib';
|
||||||
|
|
||||||
function refreshInstalledPlugins(store: Store) {
|
function refreshInstalledPlugins(store: Store) {
|
||||||
getPendingAndInstalledPlugins().then((plugins) =>
|
getInstalledPlugins().then((plugins) =>
|
||||||
store.dispatch(registerInstalledPlugins(plugins)),
|
store.dispatch(registerInstalledPlugins(plugins)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {default as reducer, registerInstalledPlugins} from '../pluginManager';
|
import {default as reducer, registerInstalledPlugins} from '../pluginManager';
|
||||||
|
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||||
|
|
||||||
test('reduce empty registerInstalledPlugins', () => {
|
test('reduce empty registerInstalledPlugins', () => {
|
||||||
const result = reducer(undefined, registerInstalledPlugins(new Map()));
|
const result = reducer(undefined, registerInstalledPlugins([]));
|
||||||
expect(result).toEqual({installedPlugins: new Map()});
|
expect(result).toEqual({installedPlugins: []});
|
||||||
});
|
});
|
||||||
|
|
||||||
const EXAMPLE_PLUGIN = {
|
const EXAMPLE_PLUGIN = {
|
||||||
@@ -26,17 +27,15 @@ const EXAMPLE_PLUGIN = {
|
|||||||
title: 'test',
|
title: 'test',
|
||||||
id: 'test',
|
id: 'test',
|
||||||
entry: '/plugins/test/lib/index.js',
|
entry: '/plugins/test/lib/index.js',
|
||||||
};
|
installationStatus: 'installed',
|
||||||
|
} as InstalledPluginDetails;
|
||||||
|
|
||||||
test('reduce registerInstalledPlugins, clear again', () => {
|
test('reduce registerInstalledPlugins, clear again', () => {
|
||||||
const result = reducer(
|
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
||||||
undefined,
|
|
||||||
registerInstalledPlugins(new Map([['test', EXAMPLE_PLUGIN]])),
|
|
||||||
);
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
installedPlugins: new Map([['test', EXAMPLE_PLUGIN]]),
|
installedPlugins: [EXAMPLE_PLUGIN],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result2 = reducer(result, registerInstalledPlugins(new Map()));
|
const result2 = reducer(result, registerInstalledPlugins([]));
|
||||||
expect(result2).toEqual({installedPlugins: new Map()});
|
expect(result2).toEqual({installedPlugins: []});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,19 +8,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Actions} from './';
|
import {Actions} from './';
|
||||||
import {PluginMap} from 'flipper-plugin-lib';
|
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
installedPlugins: PluginMap;
|
installedPlugins: InstalledPluginDetails[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action = {
|
export type Action = {
|
||||||
type: 'REGISTER_INSTALLED_PLUGINS';
|
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||||
payload: PluginMap;
|
payload: InstalledPluginDetails[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const INITIAL_STATE: State = {
|
const INITIAL_STATE: State = {
|
||||||
installedPlugins: new Map(),
|
installedPlugins: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function reducer(
|
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',
|
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||||
payload,
|
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",
|
"license": "MIT",
|
||||||
"bugs": "https://github.com/facebook/flipper/issues",
|
"bugs": "https://github.com/facebook/flipper/issues",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@algolia/client-search": "^4.4.0",
|
||||||
|
"algoliasearch": "^4.4.0",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"decompress-targz": "^4.1.1",
|
"decompress-targz": "^4.1.1",
|
||||||
"decompress-unzip": "^4.0.1",
|
"decompress-unzip": "^4.0.1",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"live-plugin-manager": "^0.14.1",
|
"live-plugin-manager": "^0.14.1",
|
||||||
|
"npm-api": "^1.0.0",
|
||||||
|
"p-map": "^4.0.0",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"tmp": "^0.2.1"
|
"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 PluginDetails} from './PluginDetails';
|
||||||
export {default as getPluginDetails} from './getPluginDetails';
|
export {default as getPluginDetails} from './getPluginDetails';
|
||||||
export * from './pluginInstaller';
|
export * from './pluginInstaller';
|
||||||
|
export * from './getInstalledPlugins';
|
||||||
|
export * from './getUpdatablePlugins';
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import {
|
|||||||
} from './pluginPaths';
|
} from './pluginPaths';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
export type PluginMap = Map<string, PluginDetails>;
|
|
||||||
|
|
||||||
const getTmpDir = promisify(tmp.dir) as () => Promise<string>;
|
const getTmpDir = promisify(tmp.dir) as () => Promise<string>;
|
||||||
|
|
||||||
function providePluginManager(): PM {
|
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(
|
return path.join(
|
||||||
pluginInstallationDir,
|
pluginInstallationDir,
|
||||||
replaceInvalidPathSegmentCharacters(name),
|
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> {
|
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() {
|
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 {
|
export interface Package {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
[name: string]: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,150 +12,109 @@
|
|||||||
resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f"
|
resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f"
|
||||||
integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA==
|
integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA==
|
||||||
|
|
||||||
"@algolia/cache-browser-local-storage@4.1.0":
|
"@algolia/cache-browser-local-storage@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.1.0.tgz#c4f1bfc57ea562248072b35831e3c4b646cc3921"
|
resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.4.0.tgz#f58055bdf798d7b31b6d5f86e465cb0fc7dd6694"
|
||||||
integrity sha512-r8BOgqZXVt+JPgP19PQNzZ+lYP+MP6eZKNQqfRYofFEx+K9oyfdtGCqmoWJsBUi3nNOzhbOcg2jfP2GJzJBZ5g==
|
integrity sha512-2AiKgN7DpFypkRCRkpqH7waXXyFdcnsPWzmN8sLHrB/FfXqgmsQb3pGft+9YHZIDQ0vAnfgMxSGgMhMGW+0Qnw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/cache-common" "4.1.0"
|
"@algolia/cache-common" "4.4.0"
|
||||||
|
|
||||||
"@algolia/cache-common@4.1.0":
|
"@algolia/cache-common@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.1.0.tgz#ab895f8049ff7064ca1bfea504a56f97fd5d4683"
|
resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.4.0.tgz#bfe84790230f5d2de495238b29e9397c5ed2b26e"
|
||||||
integrity sha512-ZvvK40bs1BWLErchleZL4ctHT2uH56uLMnpZPCuIk+H2PKddeiIQc/z2JDu2BHr68u513XIAAoQ+C+LgKNugmw==
|
integrity sha512-PrIgoMnXaDWUfwOekahro543pgcJfgRu/nd/ZQS5ffem3+Ow725eZY6HDpPaQ1k3cvLii9JH6V2sNJConjqUKA==
|
||||||
|
|
||||||
"@algolia/cache-common@4.3.0":
|
"@algolia/cache-in-memory@4.4.0":
|
||||||
version "4.3.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.3.0.tgz#3a257b184bce678e524e354c4f4abd3235ccd24d"
|
resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.4.0.tgz#54a089094c2afa5b9cacab4b60a5f1ba29013a7c"
|
||||||
integrity sha512-AHTbOn9lk0f5IkjssXXmDgnaZfsUJVZ61sqOH1W3LyJdAscDzCj0KtwijELn8FHlLXQak7+K93/O3Oct0uHncQ==
|
integrity sha512-9+XlUB0baDU/Dp9URRHPp6Q37YmTO0QmgPWt9+n+wqZrRL0jR3Jezr4jCT7RemqGMxBiR+YpnqaUv0orpb0ptw==
|
||||||
|
|
||||||
"@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==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/cache-common" "4.1.0"
|
"@algolia/cache-common" "4.4.0"
|
||||||
|
|
||||||
"@algolia/client-account@4.1.0":
|
"@algolia/client-account@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.1.0.tgz#a31d26c22e6a56554ea4aa8552d153b1a1aa4363"
|
resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.4.0.tgz#7dbeff83e1c85d853b3ad224674a924e02b94d1b"
|
||||||
integrity sha512-GFINlsxAHM/GEeDBjoTx8+J1ra9SINQCuXi2C9QSLFClPKug2lzApm8niJJGXckhyZ2aDLb7drJ1qJ8bTspApw==
|
integrity sha512-Kynu3cMEs0clTLf674rtrCF+FWR/JwlQxKlIWsPzvLBRmNXdvYej9YBcNaOr4OTQFCCZn9JVE8ib91Z7J4IL1Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/client-common" "4.1.0"
|
"@algolia/client-common" "4.4.0"
|
||||||
"@algolia/client-search" "4.1.0"
|
"@algolia/client-search" "4.4.0"
|
||||||
"@algolia/transporter" "4.1.0"
|
"@algolia/transporter" "4.4.0"
|
||||||
|
|
||||||
"@algolia/client-analytics@4.1.0":
|
"@algolia/client-analytics@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.1.0.tgz#eb05ccb636351b2d6494b2affb6034b791236998"
|
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.4.0.tgz#50dde68b067c615fc91434c98db9b5ca429be33d"
|
||||||
integrity sha512-JMyZ9vXGbTJWiO66fWEu9uJ7GSYfouUyaq8W/6esADPtBbelf+Nc0NRlicOwHHJGwiJvWdvELafxrhkR1+KR8A==
|
integrity sha512-GQyjQimKAc9sZbafxln9Wk7j4pEYiORv28MZkZ+0Bjt7WNXIeO7OgOOECVpQHm9buyV6hCKpNtJcbb5/syRzdQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/client-common" "4.1.0"
|
"@algolia/client-common" "4.4.0"
|
||||||
"@algolia/client-search" "4.1.0"
|
"@algolia/client-search" "4.4.0"
|
||||||
"@algolia/requester-common" "4.1.0"
|
"@algolia/requester-common" "4.4.0"
|
||||||
"@algolia/transporter" "4.1.0"
|
"@algolia/transporter" "4.4.0"
|
||||||
|
|
||||||
"@algolia/client-common@4.1.0":
|
"@algolia/client-common@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.1.0.tgz#cd3a71cef1e0d87476252cbee20b0da938f6221c"
|
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.4.0.tgz#b9fa987bc7a148f9756da59ada51fe2494a4aa9a"
|
||||||
integrity sha512-fjSMKeG54vAyQAhf+uz039/birTiLun8nDuCNx4CUbzGl97M0g96Q8jpsiZa0cjSNgh0VakMzn2GnHbS55W9/Q==
|
integrity sha512-a3yr6UhzjWPHDG/8iGp9UvrDOm1aeHVWJIf0Nj/cIvqX5tNCEIo4IMe59ovApkDgLOIpt/cLsyhn9/FiPXRhJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/requester-common" "4.1.0"
|
"@algolia/requester-common" "4.4.0"
|
||||||
"@algolia/transporter" "4.1.0"
|
"@algolia/transporter" "4.4.0"
|
||||||
|
|
||||||
"@algolia/client-common@4.3.0":
|
"@algolia/client-recommendation@4.4.0":
|
||||||
version "4.3.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.3.0.tgz#d386f67a8068e5ca2d2a00d37fab10a653744951"
|
resolved "https://registry.yarnpkg.com/@algolia/client-recommendation/-/client-recommendation-4.4.0.tgz#82410f7a346ed8518b8dcd28bc47571e850ab74f"
|
||||||
integrity sha512-8Ohj6zXZkpwDKc8ZWVTZo2wPO4+LT5D258suGg/C6nh4UxOrFOp6QaqeQo8JZ1eqMqtfb3zv5SHgW4fZ00NCLQ==
|
integrity sha512-sBszbQH46rko6w2fdEG77ma8+fAg0SDkLZGxWhv4trgcnYGUBFl2dcpEPt/6koto9b4XYlf+eh+qi6iGvYqRPg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/requester-common" "4.3.0"
|
"@algolia/client-common" "4.4.0"
|
||||||
"@algolia/transporter" "4.3.0"
|
"@algolia/requester-common" "4.4.0"
|
||||||
|
"@algolia/transporter" "4.4.0"
|
||||||
|
|
||||||
"@algolia/client-recommendation@4.1.0":
|
"@algolia/client-search@4.4.0", "@algolia/client-search@^4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/client-recommendation/-/client-recommendation-4.1.0.tgz#a0a26de4a6dd902d7ca55cf381cce3a7280d5b49"
|
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.4.0.tgz#c1e107206f3ae719cd3a9877889eea5e5cbcdc62"
|
||||||
integrity sha512-UEN/QgQwVtVH++yAs2uTuyZZQQ1p5Xs/7/FKT4Kh9/8NAyqDD49zuyq/giw8PRNhWc3C/9jiO7X4RKE8QrVWGw==
|
integrity sha512-jqWcxCUyPPHnHreoMb2PnN9iHTP+V/nL62R84XuTRDE3VgTnhm4ZnqyuRdzZQqaz+gNy5znav64TmQ9FN9WW5g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/client-common" "4.1.0"
|
"@algolia/client-common" "4.4.0"
|
||||||
"@algolia/requester-common" "4.1.0"
|
"@algolia/requester-common" "4.4.0"
|
||||||
"@algolia/transporter" "4.1.0"
|
"@algolia/transporter" "4.4.0"
|
||||||
|
|
||||||
"@algolia/client-search@4.1.0":
|
"@algolia/logger-common@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.1.0.tgz#07cc422af997e409968d3b74142e984aa71ae38c"
|
resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.4.0.tgz#8115d95d5f6227f0127d33130a9c4622cde64f6f"
|
||||||
integrity sha512-bpCYMEXUdyiopEBSHHwnrRhNEwOLstIeb0Djz+/pVuTXEr3Xg3JUoAZ8xFsCVldcXaZQpbi1/T0y3ky6xUVzfw==
|
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:
|
dependencies:
|
||||||
"@algolia/client-common" "4.1.0"
|
"@algolia/logger-common" "4.4.0"
|
||||||
"@algolia/requester-common" "4.1.0"
|
|
||||||
"@algolia/transporter" "4.1.0"
|
|
||||||
|
|
||||||
"@algolia/client-search@4.3.0":
|
"@algolia/requester-browser-xhr@4.4.0":
|
||||||
version "4.3.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.3.0.tgz#9f28df3b97d0b26605b9d6c5e69ea0df39e81c53"
|
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.4.0.tgz#f5877397ed92d2d64d08846ea969aeb559a5efb6"
|
||||||
integrity sha512-KCgcIsNMW1/0F5OILiFTddbTAKduJHRvXQS4NxY1H9gQWMTVeWJS7VZQ/ukKBiUMLatwUQHJz2qpYm9fmqOjkQ==
|
integrity sha512-V3a4hXlNch355GnWaT1f5QfXhROpsjT6sd0Znq29gAhwLqfBExhLW6Khdkv5pENC0Qy7ClVhdXFrBL9QCQer1g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/client-common" "4.3.0"
|
"@algolia/requester-common" "4.4.0"
|
||||||
"@algolia/requester-common" "4.3.0"
|
|
||||||
"@algolia/transporter" "4.3.0"
|
|
||||||
|
|
||||||
"@algolia/logger-common@4.1.0":
|
"@algolia/requester-common@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.1.0.tgz#05608dee38dfa35bfe37874683760140d471bfdc"
|
resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.4.0.tgz#0e977939aae32ff81a6d27480a71771a65db6051"
|
||||||
integrity sha512-QrE4Srf1LB7ekLzl68bFqlTrv7Wk7+GpsaGfB4xFZ9Pfv89My9p7qTVqdLlA44hEFY3fZ9csJp1/PFVucgNB4w==
|
integrity sha512-jPinHlFJEFokxQ5b3JWyjQKKn+FMy0hH99PApzOgQAYOSiFRXiPEZp6LeIexDeLLu7Y3eRt/3nHvjPKa6PmRRw==
|
||||||
|
|
||||||
"@algolia/logger-common@4.3.0":
|
"@algolia/requester-node-http@4.4.0":
|
||||||
version "4.3.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.3.0.tgz#ab01dd0458f9e5c1dd8e9ea43d604d7e4b76ad33"
|
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.4.0.tgz#6ffba93d54eeadf64cb1be67fae5c4e3f7c8f390"
|
||||||
integrity sha512-vQ+aukjZkRAyO9iyINBefT366UtF/B9QoA1Kw8PlY67T6fYmklFgYp3LNH/e7h/gz0py5LYY/HIwSsaTKk8/VQ==
|
integrity sha512-b7HC9C/GHxiV4+0GpCRTtjscvwarPr3dGm4CAhb6AkNjgjRcFUNr1NfsF75w3WVmzmt79/7QZihddztDdVMGjw==
|
||||||
|
|
||||||
"@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==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/logger-common" "4.1.0"
|
"@algolia/requester-common" "4.4.0"
|
||||||
|
|
||||||
"@algolia/requester-browser-xhr@4.1.0":
|
"@algolia/transporter@4.4.0":
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.1.0.tgz#a7ab63f184f3d0aa8e85ac73ce39c528271c6d9b"
|
resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.4.0.tgz#6ec79aac43bc515c8e4f6d6e27dc8d8cd7112f7e"
|
||||||
integrity sha512-bLMfIAkOLs1/vGA09yxU0N5+bE0fSSvEH2ySqVssfWLMP+KRAvby2Goxm8BgI9xLkOvLbhazfQ4Ov2448VvA1g==
|
integrity sha512-Xxzq91DEEeKIzT3DU46n4LEyTGAKZNtSHc2H9wvIY5MYwhZwEribmXXZ6k8W1FvBvzggv3juu0SP+xwGoR7F0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/requester-common" "4.1.0"
|
"@algolia/cache-common" "4.4.0"
|
||||||
|
"@algolia/logger-common" "4.4.0"
|
||||||
"@algolia/requester-common@4.1.0":
|
"@algolia/requester-common" "4.4.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"
|
|
||||||
|
|
||||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
|
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
|
||||||
version "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"
|
json-schema-traverse "^0.4.1"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
algoliasearch@^4.0.0:
|
algoliasearch@^4.4.0:
|
||||||
version "4.1.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.1.0.tgz#d422ac0d115497021a6c96f4b9747dbaa63f164a"
|
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.4.0.tgz#25c356d8bdcf7e3f941633f61e1ac111ddcba404"
|
||||||
integrity sha512-0lzjvqQZkJYPuv7LyQauMIMCFFzJWfUf3m9KuHjmFubwbnTDa87KCMXKouMJ0kWXXt6nTLNt0+2YRREOWx2PHw==
|
integrity sha512-Ag3wxe/nSodNl/1KbHibtkh7TNLptKE300/wnGVtszRjXivaWD6333nUpCumrYObHym/fHMHyLcmQYezXbAIWQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@algolia/cache-browser-local-storage" "4.1.0"
|
"@algolia/cache-browser-local-storage" "4.4.0"
|
||||||
"@algolia/cache-common" "4.1.0"
|
"@algolia/cache-common" "4.4.0"
|
||||||
"@algolia/cache-in-memory" "4.1.0"
|
"@algolia/cache-in-memory" "4.4.0"
|
||||||
"@algolia/client-account" "4.1.0"
|
"@algolia/client-account" "4.4.0"
|
||||||
"@algolia/client-analytics" "4.1.0"
|
"@algolia/client-analytics" "4.4.0"
|
||||||
"@algolia/client-common" "4.1.0"
|
"@algolia/client-common" "4.4.0"
|
||||||
"@algolia/client-recommendation" "4.1.0"
|
"@algolia/client-recommendation" "4.4.0"
|
||||||
"@algolia/client-search" "4.1.0"
|
"@algolia/client-search" "4.4.0"
|
||||||
"@algolia/logger-common" "4.1.0"
|
"@algolia/logger-common" "4.4.0"
|
||||||
"@algolia/logger-console" "4.1.0"
|
"@algolia/logger-console" "4.4.0"
|
||||||
"@algolia/requester-browser-xhr" "4.1.0"
|
"@algolia/requester-browser-xhr" "4.4.0"
|
||||||
"@algolia/requester-common" "4.1.0"
|
"@algolia/requester-common" "4.4.0"
|
||||||
"@algolia/requester-node-http" "4.1.0"
|
"@algolia/requester-node-http" "4.4.0"
|
||||||
"@algolia/transporter" "4.1.0"
|
"@algolia/transporter" "4.4.0"
|
||||||
|
|
||||||
ansi-align@^3.0.0:
|
ansi-align@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user