Move local plugin discovery to dispatcher/redux store
Summary: In order to have update notifications, this must live outside the UI component, but it also gives some additional benefits like better testability of previously effectful UI. Reviewed By: jknoxville Differential Revision: D18173166 fbshipit-source-id: 1cacb6c7893423a7920a6620dfb76e631caba101
This commit is contained in:
committed by
Facebook Github Bot
parent
42a77094f4
commit
432bb1b00a
@@ -30,12 +30,14 @@ import {List} from 'immutable';
|
|||||||
import algoliasearch from 'algoliasearch';
|
import algoliasearch from 'algoliasearch';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import {homedir} from 'os';
|
|
||||||
import {PluginManager as PM} from 'live-plugin-manager';
|
import {PluginManager as PM} from 'live-plugin-manager';
|
||||||
import {reportPlatformFailures, reportUsage} from '../utils/metrics';
|
import {reportPlatformFailures, reportUsage} from '../utils/metrics';
|
||||||
import restartFlipper from '../utils/restartFlipper';
|
import restartFlipper from '../utils/restartFlipper';
|
||||||
|
import {PluginMap, PluginDefinition} from '../reducers/pluginManager';
|
||||||
|
import {PLUGIN_DIR} from '../dispatcher/pluginManager';
|
||||||
|
import {State as AppState} from '../reducers';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
const PLUGIN_DIR = path.join(homedir(), '.flipper', 'thirdparty');
|
|
||||||
const ALGOLIA_APPLICATION_ID = 'OFCNCOG2CU';
|
const ALGOLIA_APPLICATION_ID = 'OFCNCOG2CU';
|
||||||
const ALGOLIA_API_KEY = 'f54e21fa3a2a0160595bb058179bfb1e';
|
const ALGOLIA_API_KEY = 'f54e21fa3a2a0160595bb058179bfb1e';
|
||||||
const TAG = 'PluginInstaller';
|
const TAG = 'PluginInstaller';
|
||||||
@@ -43,12 +45,6 @@ const PluginManager = new PM({
|
|||||||
ignoredDependencies: ['flipper', 'react', 'react-dom', '@types/*'],
|
ignoredDependencies: ['flipper', 'react', 'react-dom', '@types/*'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PluginDefinition = {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EllipsisText = styled(Text)({
|
const EllipsisText = styled(Text)({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
@@ -94,18 +90,23 @@ const RestartBar = styled(FlexColumn)({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
type PropsFromState = {
|
||||||
|
installedPlugins: PluginMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
searchIndexFactory: () => algoliasearch.Index;
|
searchIndexFactory: () => algoliasearch.Index;
|
||||||
getInstalledPlugins: () => Promise<Map<string, PluginDefinition>>;
|
|
||||||
autoHeight: boolean;
|
autoHeight: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Props = OwnProps & PropsFromState;
|
||||||
|
|
||||||
const defaultProps: Props = {
|
const defaultProps: Props = {
|
||||||
searchIndexFactory: () => {
|
searchIndexFactory: () => {
|
||||||
const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY);
|
const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY);
|
||||||
return client.initIndex('npm-search');
|
return client.initIndex('npm-search');
|
||||||
},
|
},
|
||||||
getInstalledPlugins: _getInstalledPlugins,
|
installedPlugins: new Map(),
|
||||||
autoHeight: false,
|
autoHeight: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +118,8 @@ const PluginInstaller = function props(props: Props) {
|
|||||||
query,
|
query,
|
||||||
setQuery,
|
setQuery,
|
||||||
props.searchIndexFactory,
|
props.searchIndexFactory,
|
||||||
props.getInstalledPlugins,
|
// TODO(T56693735): Refactor this to directly take props.
|
||||||
|
async () => props.installedPlugins,
|
||||||
);
|
);
|
||||||
const restartApp = useCallback(() => {
|
const restartApp = useCallback(() => {
|
||||||
restartFlipper();
|
restartFlipper();
|
||||||
@@ -155,7 +157,6 @@ const PluginInstaller = function props(props: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
PluginInstaller.defaultProps = defaultProps;
|
PluginInstaller.defaultProps = defaultProps;
|
||||||
export default PluginInstaller;
|
|
||||||
|
|
||||||
const TableButton = styled(Button)({
|
const TableButton = styled(Button)({
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
@@ -365,32 +366,9 @@ function useNPMSearch(
|
|||||||
return List(results.map(createRow));
|
return List(results.map(createRow));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _getInstalledPlugins(): Promise<Map<string, PluginDefinition>> {
|
export default connect<PropsFromState, {}, OwnProps, AppState>(
|
||||||
const pluginDirExists = await fs.pathExists(PLUGIN_DIR);
|
({pluginManager: {installedPlugins}}) => ({
|
||||||
|
installedPlugins,
|
||||||
if (!pluginDirExists) {
|
|
||||||
return new Map();
|
|
||||||
}
|
|
||||||
const dirs = await fs.readdir(PLUGIN_DIR);
|
|
||||||
const plugins = await Promise.all<[string, PluginDefinition]>(
|
|
||||||
dirs.map(
|
|
||||||
name =>
|
|
||||||
new Promise(async (resolve, reject) => {
|
|
||||||
if (!(await fs.lstat(path.join(PLUGIN_DIR, name))).isDirectory()) {
|
|
||||||
return resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const packageJSON = await fs.readFile(
|
|
||||||
path.join(PLUGIN_DIR, name, 'package.json'),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
resolve([name, JSON.parse(packageJSON.toString())]);
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
),
|
{},
|
||||||
);
|
)(PluginInstaller);
|
||||||
return new Map(plugins.filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import server from './server';
|
|||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
import plugins from './plugins';
|
import plugins from './plugins';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
|
import pluginManager from './pluginManager';
|
||||||
|
|
||||||
import {Logger} from '../fb-interfaces/Logger';
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
import {Store} from '../reducers/index';
|
import {Store} from '../reducers/index';
|
||||||
@@ -33,6 +34,7 @@ export default function(store: Store, logger: Logger): () => Promise<void> {
|
|||||||
notifications,
|
notifications,
|
||||||
plugins,
|
plugins,
|
||||||
user,
|
user,
|
||||||
|
pluginManager,
|
||||||
].filter(notNull);
|
].filter(notNull);
|
||||||
const globalCleanup = dispatchers
|
const globalCleanup = dispatchers
|
||||||
.map(dispatcher => dispatcher(store, logger))
|
.map(dispatcher => dispatcher(store, logger))
|
||||||
|
|||||||
57
src/dispatcher/pluginManager.tsx
Normal file
57
src/dispatcher/pluginManager.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Store} from '../reducers/index';
|
||||||
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import {homedir} from 'os';
|
||||||
|
import {
|
||||||
|
registerInstalledPlugins,
|
||||||
|
PluginMap,
|
||||||
|
PluginDefinition,
|
||||||
|
} from '../reducers/pluginManager';
|
||||||
|
|
||||||
|
export const PLUGIN_DIR = path.join(homedir(), '.flipper', 'thirdparty');
|
||||||
|
|
||||||
|
async function getInstalledPlugins(): Promise<PluginMap> {
|
||||||
|
const pluginDirExists = await fs.pathExists(PLUGIN_DIR);
|
||||||
|
|
||||||
|
if (!pluginDirExists) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
const dirs = await fs.readdir(PLUGIN_DIR);
|
||||||
|
const plugins = await Promise.all<[string, PluginDefinition]>(
|
||||||
|
dirs.map(
|
||||||
|
name =>
|
||||||
|
new Promise(async (resolve, reject) => {
|
||||||
|
if (!(await fs.lstat(path.join(PLUGIN_DIR, name))).isDirectory()) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJSON = await fs.readFile(
|
||||||
|
path.join(PLUGIN_DIR, name, 'package.json'),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
resolve([name, JSON.parse(packageJSON.toString())]);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return new Map(plugins.filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (store: Store, _logger: Logger) => {
|
||||||
|
getInstalledPlugins().then(plugins =>
|
||||||
|
store.dispatch(registerInstalledPlugins(plugins)),
|
||||||
|
);
|
||||||
|
};
|
||||||
34
src/reducers/__tests__/pluginManager.node.tsx
Normal file
34
src/reducers/__tests__/pluginManager.node.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* 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 reducer, registerInstalledPlugins} from '../pluginManager';
|
||||||
|
|
||||||
|
test('reduce empty registerInstalledPlugins', () => {
|
||||||
|
const result = reducer(undefined, registerInstalledPlugins(new Map()));
|
||||||
|
expect(result).toEqual({installedPlugins: new Map()});
|
||||||
|
});
|
||||||
|
|
||||||
|
const EXAMPLE_PLUGIN = {
|
||||||
|
name: 'test',
|
||||||
|
version: '0.1',
|
||||||
|
description: 'my test plugin',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('reduce registerInstalledPlugins, clear again', () => {
|
||||||
|
const result = reducer(
|
||||||
|
undefined,
|
||||||
|
registerInstalledPlugins(new Map([['test', EXAMPLE_PLUGIN]])),
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
installedPlugins: new Map([['test', EXAMPLE_PLUGIN]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result2 = reducer(result, registerInstalledPlugins(new Map()));
|
||||||
|
expect(result2).toEqual({installedPlugins: new Map()});
|
||||||
|
});
|
||||||
@@ -36,6 +36,10 @@ import settings, {
|
|||||||
Settings as SettingsState,
|
Settings as SettingsState,
|
||||||
Action as SettingsAction,
|
Action as SettingsAction,
|
||||||
} from './settings';
|
} from './settings';
|
||||||
|
import pluginManager, {
|
||||||
|
State as PluginManagerState,
|
||||||
|
Action as PluginManagerAction,
|
||||||
|
} from './pluginManager';
|
||||||
import user, {State as UserState, Action as UserAction} from './user';
|
import user, {State as UserState, Action as UserAction} from './user';
|
||||||
import JsonFileStorage from '../utils/jsonFileReduxPersistStorage';
|
import JsonFileStorage from '../utils/jsonFileReduxPersistStorage';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
@@ -56,6 +60,7 @@ export type Actions =
|
|||||||
| UserAction
|
| UserAction
|
||||||
| SettingsAction
|
| SettingsAction
|
||||||
| SupportFormAction
|
| SupportFormAction
|
||||||
|
| PluginManagerAction
|
||||||
| {type: 'INIT'};
|
| {type: 'INIT'};
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
@@ -67,6 +72,7 @@ export type State = {
|
|||||||
user: UserState & PersistPartial;
|
user: UserState & PersistPartial;
|
||||||
settingsState: SettingsState & PersistPartial;
|
settingsState: SettingsState & PersistPartial;
|
||||||
supportForm: SupportFormState;
|
supportForm: SupportFormState;
|
||||||
|
pluginManager: PluginManagerState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Store = ReduxStore<State, Actions>;
|
export type Store = ReduxStore<State, Actions>;
|
||||||
@@ -106,6 +112,7 @@ export default combineReducers<State, Actions>({
|
|||||||
),
|
),
|
||||||
plugins,
|
plugins,
|
||||||
supportForm,
|
supportForm,
|
||||||
|
pluginManager,
|
||||||
user: persistReducer(
|
user: persistReducer(
|
||||||
{
|
{
|
||||||
key: 'user',
|
key: 'user',
|
||||||
|
|||||||
50
src/reducers/pluginManager.tsx
Normal file
50
src/reducers/pluginManager.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Actions} from './';
|
||||||
|
|
||||||
|
export type PluginDefinition = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginMap = Map<string, PluginDefinition>;
|
||||||
|
|
||||||
|
export type State = {
|
||||||
|
installedPlugins: PluginMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Action = {
|
||||||
|
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||||
|
payload: PluginMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_STATE: State = {
|
||||||
|
installedPlugins: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reducer(
|
||||||
|
state: State = INITIAL_STATE,
|
||||||
|
action: Actions,
|
||||||
|
): State {
|
||||||
|
if (action.type === 'REGISTER_INSTALLED_PLUGINS') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
installedPlugins: action.payload,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const registerInstalledPlugins = (payload: PluginMap): Action => ({
|
||||||
|
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user