feat(plugins): add export-format registration hook to plugin API

Add context.formats.registerExportFormat(id, opts) to PluginContext,
backed by a new FormatRegistry (mirrors PluginRegistry's Map-based
shape). Plugins register namespaced (${pluginId}:${id}) export
formats with a label/extension/handler; the writing-studio built-in
plugin registers a trivial "sprint-summary" .txt export as a
worked example.

The plugin system lives entirely in the renderer process while the
Export menu is built in main.js, so wiring formats into the menu
required a small IPC round-trip: renderer sends format metadata to
main after plugin load (main rebuilds the menu via the already-
idempotent createMenu()), and a menu click sends the resolved save
path back to the renderer, which is the only process holding the
plugin's handler function.

Amit Haridas
This commit is contained in:
2026-08-23 19:31:33 +05:30
parent 44624cd4bf
commit d6baa2daf7
10 changed files with 304 additions and 3 deletions
+37
View File
@@ -1,6 +1,7 @@
const { PluginRegistry } = require('../src/plugins/plugin-registry');
const { PluginAPI } = require('../src/plugins/plugin-api');
const { EventBus } = require('../src/plugins/event-bus');
const { FormatRegistry } = require('../src/plugins/format-registry');
class TestPlugin extends PluginAPI {
init(context) {
@@ -153,4 +154,40 @@ describe('PluginRegistry', () => {
registry.getPlugin('test').instance.ctx.exports.registerPreHook(handler);
expect(registry.exportHooks.preHooks).toContain(handler);
});
test('a plugin calling context.formats.registerExportFormat populates the injected FormatRegistry with a namespaced entry', () => {
const formatRegistry = new FormatRegistry();
const registryWithFormats = new PluginRegistry({ ...mockDeps, formatRegistry });
class ExportingPlugin extends PluginAPI {
init(context) {
context.formats.registerExportFormat('sprint-summary', {
label: 'Writing Studio Summary (.txt)',
extension: 'txt',
handler: async () => {},
});
}
}
registryWithFormats.register({
id: 'writing-studio',
name: 'Writing Studio',
version: '1.0.0',
description: 'desc',
manifest: {},
PluginClass: ExportingPlugin,
dir: '/tmp/test',
});
const entry = formatRegistry.get('writing-studio:sprint-summary');
expect(entry).toBeDefined();
expect(entry.label).toBe('Writing Studio Summary (.txt)');
expect(entry.extension).toBe('txt');
expect(typeof entry.handler).toBe('function');
const all = formatRegistry.getAll();
expect(all).toContainEqual(
expect.objectContaining({ id: 'writing-studio:sprint-summary', extension: 'txt' })
);
});
});