@fstage/plugin is the platform-level runtime for passive plugin definitions.
It does not do discovery, trust, install persistence, or remote manifests —
those belong to higher-level systems such as Codi. Its job is narrower:
- register plugin sources
- validate plugin definition modules
- activate / deactivate plugins
- provide scoped runtime context
- enforce handler / contribution ownership
- clean up subscriptions and effects on teardown or failed activation
A plugin module default-exports a plain object:
export default {
async activate(ctx) {
// register handlers, contributions, subscriptions, UI mounts, etc.
},
async deactivate(ctx) {
// optional explicit teardown work
}
}activate(ctx) is required. deactivate(ctx) is optional.
Create a manager with createPluginManager(...):
import { createPluginManager } from '@fstage/plugin';
const plugins = createPluginManager({
createHostFacade(source) {
return {
can(capabilityId) { return capabilityId === 'app.log'; },
capability(capabilityId) {
if(capabilityId !== 'app.log') throw new Error('Unknown capability');
return (value) => console.log(value);
}
};
}
});Register a plugin source before activation:
plugins.registerSource({
manifest: {
id: 'demo.plugin',
name: 'Demo plugin',
dependsOn: ['demo.base']
},
moduleUrl: '/js/plugins/demo/index.mjs',
protected: false,
meta: { origin: 'local' }
});The default manifest contract is intentionally small:
id— required plugin idname— optional display name, defaults toiddependsOn— optional dependency ids
Apps may provide a custom normalizeManifest(...) if they need a richer
manifest shape.
Each active plugin receives ctx with:
ctx.manifestctx.can(capabilityId)ctx.host.capability(capabilityId)ctx.handlers.register(...)ctx.handlers.call(...)ctx.contributions.register(...)ctx.contributions.list(...)ctx.contributions.remove(...)ctx.events.on(...)ctx.cleanup(fn)
ctx.cleanup(fn) is for activation-scoped cleanup. These cleanup callbacks run:
- after normal deactivation
- after
deactivate(ctx)finishes - after failed activation rollback
- dependencies activate before dependents
- duplicate handler ids are rejected
- contribution ownership is enforced
plugin.internal.*handlers are only callable by the owning plugin or host- failed activation rolls back registered handlers, contributions, listeners, and cleanup callbacks
- protected sources cannot be deactivated or removed through the manager
Use higher-level code for:
- plugin discovery / search
- remote manifests and package formats
- trust / permission policy
- install state persistence
- management UI
A typical app loads @fstage/plugin through loadAssets, then composes the
trusted plugin manager in afterLoadApp via e.modules.get(...). Keep discovery,
permission checks, install state, and management UI above this runtime layer.