Skip to content

Repository files navigation

@coffic/cosy-kernel

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 | 中文版

Design principles

  • 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 by plugins/*. 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 prebuilt dist/ (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.

Exports

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

Quick start

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: definePlugin produces a PluginDefinition (declaration shape), while Kernel.registerPlugin accepts an assembled PluginRuntime — id/version are derived by the assembly layer, and providers are registered into the contribution scope during activation. This is what each project's assembler (generator) does.

Plugin definition shape

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
		},
	},
});

Lifecycle

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 by id lexicographically.
  • Providers carry a version; ProviderVersionConflictError rejects conflicting versions at the kernel level.

Development

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.

About

通用插件内核:插件注册、Provider 契约、生命周期与贡献管理(@coffic/cosy-kernel,零依赖,不感知业务领域)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages