diff --git a/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx b/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx index 8c0c557b8..66cef4d25 100644 --- a/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx +++ b/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx @@ -28,6 +28,7 @@ export function plugin(client: PluginClient) { const activateStub = jest.fn(); const deactivateStub = jest.fn(); const destroyStub = jest.fn(); + const readyStub = jest.fn(); const state = createState( { count: 0, @@ -43,6 +44,7 @@ export function plugin(client: PluginClient) { client.onActivate(activateStub); client.onDeactivate(deactivateStub); client.onDestroy(destroyStub); + client.onReady(readyStub); client.onMessage('inc', ({delta}) => { state.update((draft) => { draft.count += delta; @@ -81,6 +83,7 @@ export function plugin(client: PluginClient) { connectStub, destroyStub, disconnectStub, + readyStub, getCurrentState, state, unhandledMessages, diff --git a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx index 78917e0f0..66655cc5e 100644 --- a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx @@ -58,6 +58,7 @@ test('it can start a plugin and lifecycle events', () => { expect(instance.activateStub).toBeCalledTimes(2); expect(instance.deactivateStub).toBeCalledTimes(2); expect(instance.destroyStub).toBeCalledTimes(1); + expect(instance.readyStub).toBeCalledTimes(1); expect(instance.appName).toBe('TestApplication'); expect(instance.appId).toBe('TestApplication#Android#TestDevice#serial-000'); @@ -220,12 +221,14 @@ test('plugins support non-serializable state', async () => { }); test('plugins support restoring state', async () => { + const readyFn = jest.fn(); const {exportState, instance} = TestUtils.startPlugin( { - plugin() { + plugin(c: PluginClient<{}, {}>) { const field1 = createState(1, {persist: 'field1'}); const field2 = createState(2); const field3 = createState(3, {persist: 'field3'}); + c.onReady(readyFn); return { field1, field2, @@ -247,6 +250,7 @@ test('plugins support restoring state', async () => { expect(field3.get()).toBe('b'); expect(exportState()).toEqual({field1: 'a', field3: 'b'}); + expect(readyFn).toBeCalledTimes(1); }); test('plugins cannot use a persist key twice', async () => { @@ -267,6 +271,8 @@ test('plugins cannot use a persist key twice', async () => { }); test('plugins can have custom import handler', () => { + const readyFn = jest.fn(); + const {instance} = TestUtils.startPlugin( { plugin(client: PluginClient) { @@ -277,6 +283,7 @@ test('plugins can have custom import handler', () => { field1.set(data.a); field2.set(data.b); }); + client.onReady(readyFn); return {field1, field2}; }, @@ -293,6 +300,7 @@ test('plugins can have custom import handler', () => { ); expect(instance.field1.get()).toBe(1); expect(instance.field2.get()).toBe(2); + expect(readyFn).toBeCalledTimes(1); }); test('plugins cannot combine import handler with persist option', async () => { @@ -344,7 +352,7 @@ test('plugins can handle import errors', async () => { expect(console.error.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "Error occurred when importing date for plugin 'TestPlugin': 'Error: Oops", + "An error occurred when importing data for plugin 'TestPlugin': 'Error: Oops", [Error: Oops], ], ] diff --git a/desktop/flipper-plugin/src/plugin/PluginBase.tsx b/desktop/flipper-plugin/src/plugin/PluginBase.tsx index 56dded28b..0a8171fc8 100644 --- a/desktop/flipper-plugin/src/plugin/PluginBase.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginBase.tsx @@ -66,6 +66,13 @@ export interface BasePluginClient { */ onImport(handler: StateImportHandler): void; + /** + * The `onReady` event is triggered immediately after a plugin has been initialized and any pending state was restored. + * This event fires after `onImport` / the interpretation of any `persist` flags and indicates that the initialization process has finished. + * This event does not signal that the plugin is loaded in the UI yet (see `onActivated`) and does fire before deeplinks (see `onDeeplink`) are handled. + */ + onReady(handler: () => void): void; + /** * Register menu entries in the Flipper toolbar */ @@ -225,31 +232,40 @@ export abstract class BasePluginInstance { ); } if (this.initialStates) { - if (this.importHandler) { - try { + try { + if (this.importHandler) { batched(this.importHandler)(this.initialStates); - } catch (e) { - const msg = `Error occurred when importing date for plugin '${this.definition.id}': '${e}`; - // msg is already specific - // eslint-disable-next-line - console.error(msg, e); - message.error(msg); - } - } else { - for (const key in this.rootStates) { - if (key in this.initialStates) { - this.rootStates[key].deserialize(this.initialStates[key]); - } else { - console.warn( - `Tried to initialize plugin with existing data, however data for "${key}" is missing. Was the export created with a different Flipper version?`, - ); + } else { + for (const key in this.rootStates) { + if (key in this.initialStates) { + this.rootStates[key].deserialize(this.initialStates[key]); + } else { + console.warn( + `Tried to initialize plugin with existing data, however data for "${key}" is missing. Was the export created with a different Flipper version?`, + ); + } } } + } catch (e) { + const msg = `An error occurred when importing data for plugin '${this.definition.id}': '${e}`; + // msg is already specific + // eslint-disable-next-line + console.error(msg, e); + message.error(msg); } } this.initialStates = undefined; setCurrentPluginInstance(undefined); } + try { + this.events.emit('ready'); + } catch (e) { + const msg = `An error occurred when initializing plugin '${this.definition.id}': '${e}`; + // msg is already specific + // eslint-disable-next-line + console.error(msg, e); + message.error(msg); + } } protected createBasePluginClient(): BasePluginClient { @@ -280,6 +296,9 @@ export abstract class BasePluginInstance { } this.importHandler = cb; }, + onReady: (cb) => { + this.events.on('ready', batched(cb)); + }, addMenuEntry: (...entries) => { for (const entry of entries) { const normalized = normalizeMenuEntry(entry); diff --git a/docs/extending/flipper-plugin.mdx b/docs/extending/flipper-plugin.mdx index 03a0c9112..5637cfe92 100644 --- a/docs/extending/flipper-plugin.mdx +++ b/docs/extending/flipper-plugin.mdx @@ -192,6 +192,13 @@ export function plugin(client: PluginClient) { } ``` +### `onReady` + +The `onReady` event is triggered immediately after a plugin has been initialized and any pending state was restored. +This event fires after `onImport` / the interpretation of any `persist` flags and indicates that the initialization process has finished. +This event does not signal that the plugin is loaded in the UI yet (see `onActivated`) and does fire before deeplinks (see `onDeeplink`) are handled. +If a plugin has complex initialization logic it is recommended to put it in the `onReady` hook, as an error in the onReady hook won't cause the plugin not to be loaded. + ### Methods #### `send` diff --git a/docs/extending/sandy-migration.mdx b/docs/extending/sandy-migration.mdx index 4dc6385e5..ae6155c58 100644 --- a/docs/extending/sandy-migration.mdx +++ b/docs/extending/sandy-migration.mdx @@ -96,6 +96,7 @@ Some abstractions that used to be (for example) static methods on `FlipperPlugin | `exportPersistedState` | Use the `client.onExport` hook | | `getActiveNotifications` | Use `client.showNotification` for persistent notifications, or `message` / `notification` from `antd` for one-off notifications. | `createTablePlugin` | TBD, so these conversions can be skipped for now | +| `init` | `client.onReady` | ## Using Sandy / Ant.design to organise the plugin UI