@primafuture/contrib-kit-vue
v2.0.0
Published
Vue 3 adapter for Contrib Kit with reactive outlets, scoped registries, managed lazy loading, and SSR support.
Maintainers
Readme
@primafuture/contrib-kit-vue
Vue 3 integration for Contrib Kit Core with scoped registry ownership, reactive resolution, headless outlets, managed-lazy contributions, and SSR-safe rendering.
Installation
pnpm add @primafuture/contrib-kit-core @primafuture/contrib-kit-vue vuePeer requirements are @primafuture/contrib-kit-core >=1.0.0 <2 and vue >=3.5.0 <4. Import both packages through their public roots; internal and source subpaths are intentionally closed.
Point, registry, and plugin
import * as contribKitCore from "@primafuture/contrib-kit-core";
import * as contribKitVue from "@primafuture/contrib-kit-vue";
import * as vue from "vue";
interface MenuContext {
query: string;
}
interface MenuItemData {
readonly label: string;
}
const menuPoint = contribKitVue.defineVueExtensionPoint<
MenuContext,
MenuItemData
>()({
pointId: "example/vue/menu-items",
contractVersion: 1,
cardinality: "many",
filter(contribution, context): boolean {
return contribution.data.label.toLowerCase().includes(context.query.toLowerCase());
},
});
const registry = contribKitCore.createExtensionRegistry();
registry.rootScope.registerPoint(menuPoint);
const MenuItem = contribKitVue.defineVueContributionComponent(
(props: contribKitVue.ExtensionContributionProps<MenuContext, MenuItemData>): vue.VNode =>
vue.h(
"span",
{
"data-index": props.placement.index,
"data-count": props.placement.count,
},
props.contribution.data.label,
),
);
const registration = registry.registerContribution(menuPoint, {
contributionId: "example/vue/menu-items/home",
expectedContractVersion: 1,
data: { label: "Home" },
implementation: MenuItem,
});
await registration.whenActive();
const plugin = contribKitVue.createExtensionPlugin({ registry });
const app = vue.createApp({
setup() {
const context = vue.reactive<MenuContext>({ query: "" });
return (): vue.VNode => vue.h(contribKitVue.ExtensionOutlet, {
point: menuPoint,
context,
});
},
});
app.use(plugin);
app.mount("#app");
async function disposeHost(): Promise<void> {
app.unmount();
plugin.dispose();
await registry.dispose();
}One Vue app accepts one root Contrib Kit plugin for its lifetime. The plugin owns only its Vue adapter scope; the host still owns the Core registry.
Managed lazy contributions
const lazyMenuItem = contribKitVue.defineVueLazyContribution<MenuContext, MenuItemData>({
async load({ signal }) {
const module = await import("./LazyMenuItem.js");
if (signal.aborted) {
throw new DOMException("Aborted", "AbortError");
}
return contribKitVue.defineVueContributionComponent(module.default);
},
});The server renders the outlet's loading state without starting the loader. On the client, each mounted boundary generation owns one loader attempt and abort signal. Removal, replacement, registry switching, or disposal invalidates the old generation before abort dispatch.
Outlet slots
ExtensionOutlet has three slots scoped to one contribution boundary and two slots scoped to the outlet's resolve result:
| Slot | Scope | Meaning |
| --- | --- | --- |
| contribution-ready | Each contribution | An eager or loaded managed-lazy component is ready for a render attempt. The slot replaces the default component render. |
| contribution-loading | Each contribution | One managed-lazy contribution is waiting for its current client loader generation. |
| contribution-error | Each contribution | One contribution generation failed and the effective error policy selected its fallback. |
| empty | Whole outlet | Resolution succeeded but returned no contributions. |
| resolve-error | Whole outlet | The current resolve attempt failed and can be refreshed. |
All slots are optional. Omitting contribution-ready lets the outlet render the resolved component directly. The per-contribution slots can be invoked independently for several contributions in the same outlet; empty and resolve-error describe one global outlet result.
<contribKitVue.ExtensionOutlet :point="menuPoint" :context="context">
<template #contribution-loading="state">
<span>Loading {{ state.contribution.contributionId }}</span>
</template>
<template #contribution-error="state">
<button type="button" @click="state.retry()">Retry {{ state.error.code }}</button>
</template>
<template #empty>
<span>No menu items</span>
</template>
<template #resolve-error="state">
<button type="button" @click="state.refresh()">Resolve again</button>
</template>
</contribKitVue.ExtensionOutlet>Logical placement
Every contribution component and every contribution-scoped ready, loading, or error slot receives a required frozen ResolvedContributionPlacement value through placement, with index, count, isFirst, and isLast. Placement describes the contribution's position in the final resolved membership after Core filtering, ordering, and selection. It is deliberately independent of the eventual DOM position: slots and contribution components may render fragments, wrappers, portals, or no element at all.
Retained memberships receive the newest context and placement atomically without remounting their component or restarting an active lazy generation. A renderer can therefore use placement.isFirst or placement.isLast for presentation while preserving component-local state across reordering.
Migrating from Vue adapter 1.x
- Contribution components must accept the current
ExtensionContributionProps, including its requiredplacementmember. - Contribution-scoped ready, loading, and error slots receive the same required placement snapshot.
- The successful-empty reason
filteredOutwas replaced byexcludedByPolicywithout an alias. It covers any non-empty raw catalog that becomes empty after Core filter, ordering, or selection policy evaluation. emptyandresolve-errorremain outlet-global and do not receive placement.
Nested registries and reactive context
Call provideExtensionRegistry() from component setup to override the nearest plugin registry for one subtree. An explicit registry on ExtensionOutlet or useResolvedContributions() takes precedence over the nearest provider. Disposing a plugin or provider invalidates only its adapter subtree and never disposes the Core registry.
Resolution tracks only reactive context properties synchronously read by the Core context validator, filter, or ordering policy. Changing an unread property is a no-op; conditional dependencies are collected again on every resolve attempt. Sink, projector, emit, and render callbacks are not resolve dependencies.
For SSR, create a request-local registry, await every required registration's whenActive(), then create the plugin and app. Cleanup remains host-owned: unmount the app, dispose the plugin, and finally dispose the registry.
License
ISC © 2026 PrimaFuture.cz s.r.o.
