The generic plugin kernel shared by cofficlab, kuaiyizhi-web, and other CofficLab sites.
The core of the plugin architecture: plugin registration, Provider contracts, lifecycle management, and contribution tracking. Every plugin's definePlugin comes from here.
📖 English | 中文版
- The kernel knows nothing about business domains. Contracts for blogs, products, courses, experiments, etc. are defined by each project's own
provider-*packages and consumed byplugins/*. The kernel only provides registration, resolution, lifecycle, and scope-based cleanup. - Zero dependencies: pure TypeScript with no framework coupling (no Astro / Vue / any runtime). Node 22.6+ can run it directly from source with
--experimental-strip-types; a prebuiltdist/(JS + d.ts) is committed so git dependencies work out of the box. - Contracts as code: Providers are defined with
defineProviderToken<T>(id); version conflicts are rejected at the kernel level.
| Export | Description |
|---|---|
definePlugin(manifest) |
Plugin definition entry; the default export of a plugin's src/index.ts |
definePluginManifest(manifest) |
Structured plugin manifest (routes / dependencies / ordering) |
defineProviderToken<T>(id) |
Define a Provider contract token |
Kernel |
The kernel class: registerHostProvider / registerPluginProvider / resolve / has / registerPlugin / start / stop |
createContribution(ownerPluginID, cleanup) |
Create a contribution handle that is revoked automatically on plugin stop |
PluginContext |
activate callback context: { pluginID, kernel, options, contributions } |
ContributionHandle / ContributionScope |
Contribution tracking and automatic revocation |
PluginRuntime / KernelLifecycleState |
Runtime plugin shape and lifecycle states |
import {
Kernel,
defineProviderToken,
definePlugin,
type PluginRuntime,
} from "@coffic/cosy-kernel";
const GreetingToken = defineProviderToken<{ greet: () => string }>("app.greeting");
// 1. Declare the plugin: definePlugin produces a PluginDefinition (manifest + providers + runtime)
const definition = definePlugin({
id: "my-plugin",
version: "1.0.0",
providers: [
{
token: GreetingToken,
create: () => ({ greet: () => "hello from plugin" }),
},
],
runtime: {
async activate({ pluginID, kernel }) {
console.log(`${pluginID}:`, kernel.resolve(GreetingToken).greet());
},
},
});
// 2. Assembly-layer responsibility: turn the PluginDefinition into a PluginRuntime
// (providers are registered into the plugin's contribution scope and revoked on stop)
const plugin: PluginRuntime = {
id: definition.manifest.id!,
version: definition.manifest.version!,
async activate(context) {
for (const registration of definition.providers) {
context.contributions.track(
context.kernel.registerPluginProvider(
context.pluginID,
registration.token,
await registration.create(context),
),
);
}
await definition.runtime?.activate?.(context);
},
};
const kernel = new Kernel();
kernel.registerHostProvider(GreetingToken, { greet: () => "hello cosy" });
kernel.registerPlugin(plugin);
await kernel.start();
await kernel.stop();Note:
definePluginproduces aPluginDefinition(declaration shape), whileKernel.registerPluginaccepts an assembledPluginRuntime—id/versionare derived by the assembly layer, and providers are registered into the contribution scope during activation. This is what each project's assembler (generator) does.
export default definePlugin({
id: "my-plugin",
version: "1.0.0",
order: 10,
dependencies: ["base-plugin"],
routes: [
{
id: "about",
pattern: "/about",
component: "./pages/About.astro",
prerender: true,
},
],
providers: [
{ token: SomeToken, create: () => new SomeProvider() },
],
runtime: {
async activate({ pluginID, kernel, contributions }) {
// Runtime contributions: resolve + track, auto-cleaned when the plugin stops
},
},
});created → starting → running → stopped; a failed start rolls back to failed and revokes the contributions of already-activated plugins in reverse order.
- Missing dependencies and dependency cycles are validated before activation and throw typed errors (
PluginDependencyMissingError/PluginDependencyCycleError). - Plugins activate in ascending
order, then byidlexicographically. - Providers carry a version;
ProviderVersionConflictErrorrejects conflicting versions at the kernel level.
pnpm install
pnpm check # tsc --noEmit (strict)
pnpm test # node:test unit tests
pnpm build # emit dist/ (JS + d.ts)Test coverage: lifecycle state machine, dependency ordering and cycles, activation-failure rollback, provider registration/resolution/version conflicts, contribution-scope cleanup, and invalid lifecycle operations.