mirror of
https://github.com/penpot/penpot.git
synced 2026-10-10 21:21:33 -04:00
🐛 Keep background plugins registered when another plugin opens (#11929)
Loading a plugin closed every non-background plugin and then emptied the runtime registry, dropping background plugins (allowBackground, such as the MCP plugin) that were left running. The registry is what routes a plugin iframe's postMessage traffic to its plugin and what ɵunloadPlugin searches, so after the user opened any other plugin the MCP plugin never received task requests from its UI (MCP tool calls timed out) and could no longer be unloaded. Keep background plugins in the registry and drop only the plugins that were closed. AI-assisted-by: claude-opus-5.5 Signed-off-by: breken-ai <312387581+breken-ai@users.noreply.github.com> Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com>
This commit is contained in:
1 parent
753e98bb9f
commit
b41445d092
4 files changed
+52
-3
No files matched your search
@@ -26,7 +26,7 @@
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- Loading a plugin closes existing non-background plugins and resets the runtime registry. Be careful around `allowBackground` semantics when changing load/close behavior.
|
||||
- Loading a plugin closes existing non-background plugins and removes them from the runtime registry. Background plugins (`allowBackground`) keep running and must stay registered: the registry routes their UI `postMessage` traffic by sender iframe and is what `ɵunloadPlugin` searches.
|
||||
- If sandbox evaluation fails, the runtime marks the error as plugin-originated, closes the plugin, and rethrows.
|
||||
- `plugin-manager` removes event listeners, timers, intervals, and modal state on close, and marks the plugin destroyed. Listener callbacks check that flag because Penpot events can fire after close.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- **plugins-runtime**: Removed the premature deep-hardening of the host plugin context, which froze shared host functions (including `Function.prototype`) before SES override taming, causing `TypeError: Cannot assign to read only property 'toString'` on later host-side function extension. Related to #11001.
|
||||
- **plugins-runtime**: The plugin modal no longer shows two resize grips in Firefox. Firefox now shows only its native grip. Closes #11795.
|
||||
- **plugins-runtime**: Fixed the `fontFamilies` token property mapping so `Shape.applyToken(token, ["fontFamilies"])` resolves to the canonical `:font-family` attribute and applied-token readback exposes the documented `fontFamilies` key instead of the undocumented singular `fontFamily`. Closes #11405.
|
||||
- **plugins-runtime**: Opening a plugin no longer drops running background plugins (`allowBackground`, such as the MCP plugin) from the runtime registry. They kept running, but their UI messages were no longer delivered to the plugin and they could no longer be unloaded, so MCP tasks timed out after another plugin was opened.
|
||||
|
||||
## 1.5.0 (2026-07-08)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
loadPlugin,
|
||||
ɵloadPlugin,
|
||||
ɵloadPluginByUrl,
|
||||
ɵunloadPlugin,
|
||||
setContextBuilder,
|
||||
getPlugins,
|
||||
} from './load-plugin';
|
||||
@@ -200,6 +201,47 @@ describe('plugin-loader', () => {
|
||||
expect(mockPluginApi1.plugin.sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should keep background plugins registered when loading another plugin', async () => {
|
||||
const backgroundIframeWindow = { nodeType: 1 } as unknown as Window;
|
||||
const backgroundClose = vi.fn();
|
||||
const backgroundPluginApi = {
|
||||
plugin: {
|
||||
close: backgroundClose,
|
||||
sendMessage: vi.fn(),
|
||||
},
|
||||
iframeWindow: backgroundIframeWindow,
|
||||
manifest: {
|
||||
...manifest,
|
||||
pluginId: 'background-plugin',
|
||||
allowBackground: true,
|
||||
},
|
||||
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
||||
|
||||
vi.mocked(createPlugin).mockResolvedValue(backgroundPluginApi);
|
||||
await loadPlugin(manifest);
|
||||
|
||||
vi.mocked(createPlugin).mockResolvedValue(mockPluginApi);
|
||||
await loadPlugin(manifest);
|
||||
|
||||
expect(backgroundClose).not.toHaveBeenCalled();
|
||||
expect(getPlugins()).toContain(backgroundPluginApi);
|
||||
|
||||
const event = new MessageEvent('message', { data: 'from-background' });
|
||||
Object.defineProperty(event, 'source', { value: backgroundIframeWindow });
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(backgroundPluginApi.plugin.sendMessage).toHaveBeenCalledWith(
|
||||
'from-background',
|
||||
);
|
||||
|
||||
ɵunloadPlugin('background-plugin');
|
||||
expect(backgroundClose).toHaveBeenCalledTimes(1);
|
||||
|
||||
// the runtime's close callback deregisters the plugin
|
||||
vi.mocked(createPlugin).mock.calls[0][2]();
|
||||
expect(getPlugins()).not.toContain(backgroundPluginApi);
|
||||
});
|
||||
|
||||
it('should load plugin using ɵloadPlugin', async () => {
|
||||
await ɵloadPlugin(manifest);
|
||||
|
||||
|
||||
@@ -17,14 +17,20 @@ export function setContextBuilder(builder: ContextBuilder) {
|
||||
export const getPlugins = () => plugins;
|
||||
|
||||
const closeAllPlugins = () => {
|
||||
// Background plugins keep running, so they must stay registered: the
|
||||
// registry routes their UI messages and lets them be unloaded later.
|
||||
const backgroundPlugins: typeof plugins = [];
|
||||
|
||||
plugins.forEach((pluginApi) => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
if (!(pluginApi.manifest as any)?.allowBackground) {
|
||||
if ((pluginApi.manifest as any)?.allowBackground) {
|
||||
backgroundPlugins.push(pluginApi);
|
||||
} else {
|
||||
pluginApi.plugin.close();
|
||||
}
|
||||
});
|
||||
|
||||
plugins = [];
|
||||
plugins = backgroundPlugins;
|
||||
};
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
|
||||
Reference in new issue
Block a user