| title | Ship a plugin from a library |
|---|---|
| description | Package a Pangular tab and its agent tools with your library, so every app that uses the library gets them. |
Write a plugin builds a plugin inside one app. This guide puts the same parts in a library package, so the apps that install the library get its tab without writing one.
The repository has a working example: examples/pangular-plugin-example, a small feature flag store with its own Feature flags tab and two agent tools. The Analog demo (examples/analog) uses it. The code on this page comes from those two folders.
Keep the devtools code out of the library's main entry point. An app that never loads Pangular Inspector then never loads the plugin either.
| Entry point | File | What it holds |
|---|---|---|
. |
src/index.ts |
The library itself. It imports nothing from Pangular Inspector. |
./pangular |
src/pangular.ts |
The page plugin and a register… helper. Runs in the browser. |
./pangular-server |
src/pangular-server.ts |
The server plugin with the agent tools. Runs in Node. |
Both devtools entry points import @pangular-inspector/devtools/plugin only. That entry point is small and imports neither the overlay nor the server.
Declare the devtools as an optional peer dependency, so apps without them install the library without a warning. The example's package.json:
{
"name": "pangular-plugin-example",
"type": "module",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./pangular": "./src/pangular.ts",
"./pangular-server": "./src/pangular-server.ts"
},
"peerDependencies": {
"@pangular-inspector/devtools": "^1.0.0"
},
"peerDependenciesMeta": {
"@pangular-inspector/devtools": {
"optional": true
}
}
}The example points exports at TypeScript sources because it lives in this workspace. A published library points them at its built JavaScript and type declarations.
globalThis, and every copy of the /plugin entry point shares it. If your build bundles its own copy of the entry point, the overlay still finds your plugin.
Turn your library's state into a PluginView: a summary, badges, sections and the actions the panel shows as buttons.
Give it an id, a version, a collect function and the page actions. If your state changes outside Angular, push a view from setup on each change.
Wrap registerPangularPlugin(), so apps don't build the plugin themselves.
The flag store calls its listeners on each change, which Angular does not see. The plugin subscribes in setup and returns the unsubscribe function as its cleanup:
// examples/pangular-plugin-example/src/pangular.ts
import {registerPangularPlugin, type PangularPagePlugin} from '@pangular-inspector/devtools/plugin';
import {FLAGS_PLUGIN_ID, FLAGS_PLUGIN_VERSION} from './ids.ts';
import type {FlagStore} from './index.ts';
// flagsView(store) builds the PluginView; it is defined just above this in the same file.
export function flagsPangularPlugin(store: FlagStore): PangularPagePlugin {
return {
id: FLAGS_PLUGIN_ID,
version: FLAGS_PLUGIN_VERSION,
title: 'Feature flags',
description: 'Flags from the example feature flag store, with their defaults and overrides.',
setup: (ctx) => store.subscribe(() => ctx.push(flagsView(store))),
collect: () => flagsView(store),
actions: {
set: (args) => {
const {name, value} = (args ?? {}) as {name?: unknown; value?: unknown};
if (typeof name !== 'string' || typeof value !== 'boolean') {
throw new Error('Pass { name: string, value: boolean }.');
}
if (!store.set(name, value)) throw new Error(`There is no flag ${name}.`);
return {name, value};
},
reset: () => ({reset: store.reset()}),
},
};
}
export function registerFlagsPangularPlugin(store: FlagStore): () => void {
return registerPangularPlugin(flagsPangularPlugin(store));
}flagsView(), in the same file, returns a stats section and a table of flags. Its full source is in examples/pangular-plugin-example/src/pangular.ts. It lists the reset action while a flag is overridden. An error a page action throws reaches the agent as the tool's answer. The sections table lists every section kind.
Keep the plugin id and version in a file both entry points import (src/ids.ts in the example), so the page plugin and the server plugin always match.
The server plugin adds agent tools over what the page reports. A tool that runs a page action declares safety: 'action', so agent.readOnly and the action switches drop it:
// examples/pangular-plugin-example/src/pangular-server.ts
import {definePangularServerPlugin} from '@pangular-inspector/devtools/plugin';
import {FLAGS_PLUGIN_ID, FLAGS_PLUGIN_VERSION} from './ids.ts';
const PAGE = {
type: 'string',
description: 'Page id, when more than one tab reports (see pangular:list-pages).',
};
const pageArg = (args: Record<string, unknown>) =>
typeof args['page'] === 'string' ? args['page'] : undefined;
export const flagsServerPlugin = definePangularServerPlugin({
id: FLAGS_PLUGIN_ID,
version: FLAGS_PLUGIN_VERSION,
title: 'Feature flags',
tools: [
{
name: 'flags',
description: 'Every flag in the feature flag store on the running page.',
inputSchema: {type: 'object', properties: {page: PAGE}},
handler: (args, ctx) => {
const page = ctx.page(pageArg(args));
if (!page?.view) return 'No page runs the feature flag store. Open the app in a browser.';
return ctx.markdown(page.view);
},
},
{
name: 'set',
safety: 'action',
description: 'Turn one flag on or off on the running page until it reloads.',
inputSchema: {
type: 'object',
properties: {name: {type: 'string'}, value: {type: 'boolean'}, page: PAGE},
required: ['name', 'value'],
},
handler: async (args, ctx) => {
const result = await ctx.runPageAction(
'set',
{name: args['name'], value: args['value']},
{page: pageArg(args)},
);
if (!result.ok) return result.error ?? 'The action failed.';
return `Turned \`${String(args['name'])}\` ${args['value'] ? 'on' : 'off'} on page \`${result.pageId}\`.`;
},
},
],
});The tools register as pangular:plugin:feature-flags:flags and pangular:plugin:feature-flags:set. See Add agent tools for the naming and length rules.
Your library lists @pangular-inspector/devtools as an optional peer dependency, so an app that wants the tab installs it as its own development dependency first:
pnpm add -D @pangular-inspector/devtoolsnpm install -D @pangular-inspector/devtoolsyarn add -D @pangular-inspector/devtoolsbun add -D @pangular-inspector/devtoolsThen tell the apps that use your library to make two changes.
Load the./pangular entry point with a dynamic import() in the browser, in development builds only.
Add it to serverPlugins of the Vite plugin or of initPangularHub().
The Analog demo keeps its flags in a FeatureFlags service and registers the tab from an app initializer. The dynamic import keeps the page entry out of the main bundle:
// From examples/analog/src/app/shared/feature-flags.ts
export function provideFeatureFlagsPangularPlugin(): EnvironmentProviders {
return provideAppInitializer(() => {
if (typeof ngDevMode !== 'undefined' && !ngDevMode) return;
if (!isPlatformBrowser(inject(PLATFORM_ID))) return;
const flags = inject(FeatureFlags);
const destroyRef = inject(DestroyRef);
void import('pangular-plugin-example/pangular').then(({registerFlagsPangularPlugin}) => {
if (destroyRef.destroyed) return;
destroyRef.onDestroy(registerFlagsPangularPlugin(flags.store));
});
});
}The complete file, with its imports and the FeatureFlags service, is examples/analog/src/app/shared/feature-flags.ts. It needs Angular 22 or later, since it uses @Service(), DestroyRef.destroyed and provideAppInitializer(). Add provideFeatureFlagsPangularPlugin() to the app's providers. Then pass the server plugin to the Vite plugin:
// examples/analog/vite.config.ts
import analog from '@analogjs/platform';
import pangular from '@pangular-inspector/devtools/vite';
import {flagsServerPlugin} from 'pangular-plugin-example/pangular-server';
import {defineConfig} from 'vite';
export default defineConfig(() => ({
plugins: [analog(), pangular({serverPlugins: [flagsServerPlugin]})],
}));An Express app passes the same array to initPangularHub({serverPlugins}), as Add agent tools shows. Either way, the app must load the overlay, or no plugin view appears in the panel.
Run pnpm analog:dev and open the panel. The Plugins tab lists Feature flags. Ask an agent to turn promo-banner on, and the banner appears on the home page.
Test the plugin the way the repository tests the example: packages/devtools/src/__tests__/plugin-example.test.ts registers the page plugin, runs it with the overlay's plugins collector, boots a devframe with the server plugin, and wires the two together. An agent tool call then runs the page action and returns its answer, with no browser.
For a library outside this repository, test flagsView() and the actions against your own store, and the tool handlers against a stub context that returns a page from page().