npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@oddcommon/datocms-plugin-kit

v0.1.1

Published

A toolkit for building organized, modular DatoCMS plugins

Readme

@oddcommon/datocms-plugin-kit

A toolkit for building organized, modular DatoCMS plugins.

Problem

The DatoCMS plugin SDK requires a large configuration object with 45+ hooks. Managing all these hooks in a single file quickly becomes unwieldy and difficult to maintain. This library provides a factory-function-based approach that lets you split your plugin configuration across multiple files for better organization and modularity.

Installation

pnpm add @oddcommon/datocms-plugin-kit

Requirements

  • React 19.x - This library requires React 19 or later as a peer dependency
  • Node.js 22+ - For development and building

Usage

1. Create Plugin Configuration

// ~/plugin.ts
import { createPluginConfig } from '@oddcommon/datocms-plugin-kit';

export const {
  addFormOutlet,
  addPage,
  addMainNavigationTab,
  addSidebarPanel,
  customBlockStylesForStructuredTextField,
  customMarksForStructuredTextField,
  connect,
} = createPluginConfig({
  // Optional: custom render function
  // If omitted, uses React 19's createRoot with StrictMode
  render: (component) => {
    // your custom render
  },
});

2. Register Hooks from Any File

// ~/outlets/demo.tsx
import { addFormOutlet } from '~/plugin';

addFormOutlet({
  id: 'demo-sync',
  component: DemoOutlet,
  initialHeight: 0,
  shouldApply: (model, ctx) => {
    const fields = Object.values(ctx.fields).map(f => f?.attributes.api_key);
    return fields.includes('demo_event_id');
  },
});

function DemoOutlet({ ctx }) {
  return <div>Your outlet UI</div>;
}
// ~/pages/analytics.tsx
import { addPage, addMainNavigationTab } from '~/plugin';

const pageId = 'analytics';

addMainNavigationTab({
  label: 'Analytics',
  icon: 'chart-line',
  pointsTo: { pageId },
});

addPage({
  pageId,
  component: AnalyticsPage,
});

function AnalyticsPage({ ctx }) {
  return <div>Analytics page</div>;
}

3. Connect in Main Entry

// ~/main.ts
import '~/outlets/demo';
import '~/pages/analytics';
// ... other imports

import { connect } from '~/plugin';

connect();

API Reference

Core

createPluginConfig(options?)

Creates a plugin toolkit with all registration functions.

Options:

  • render?: (component: React.ReactNode) => void - Custom render function. Defaults to React 19's createRoot with StrictMode.

Returns: Object with all registration functions and connect().

Pages

addPage(config)

Register a page component.

Config:

  • pageId: string - Unique identifier for the page
  • component: React.ComponentType<{ ctx: RenderPageCtx }> - Page component
addPage({
  pageId: 'my-page',
  component: MyPageComponent,
});

addMainNavigationTab(config)

Add a tab to the top navigation bar.

Config:

  • label: string - Tab label
  • icon: Icon - Icon (string or SVG object)
  • pointsTo: { pageId: string } - Target page ID
  • placement?: [string, string] - Optional placement
  • rank?: number - Optional ordering
addMainNavigationTab({
  label: 'Analytics',
  icon: 'chart-line',
  pointsTo: { pageId: 'analytics-page' },
});

addContentAreaSidebarItem(config)

Add an item to the content area sidebar.

Config:

  • label: string - Item label
  • icon: Icon - Icon
  • pointsTo: { pageId: string } - Target page ID
  • placement?: [string, string] - Optional placement
  • rank?: number - Optional ordering

addSettingsAreaSidebarItem(config)

Add an item to the settings area sidebar. Same config as addContentAreaSidebarItem.

Form Outlets

addFormOutlet(config)

Add an outlet to item forms.

Config:

  • id: string - Unique outlet ID
  • component: React.ComponentType<{ ctx: RenderItemFormOutletCtx }> - Outlet component
  • initialHeight?: number - Initial height in pixels (default: 0)
  • shouldApply?: (model: ItemType, ctx: ItemFormOutletsCtx) => boolean - Conditional display
addFormOutlet({
  id: 'custom-outlet',
  component: CustomOutletComponent,
  initialHeight: 200,
  shouldApply: (model, ctx) => {
    return model.attributes.api_key === 'article';
  },
});

addCollectionOutlet(config)

Add an outlet to collection views. (Note: Not yet fully implemented in Tier 1)

Sidebars

addSidebarPanel(config)

Add a collapsible panel to the item form sidebar.

Config:

  • id: string - Unique panel ID
  • component: React.ComponentType<{ ctx: RenderItemFormSidebarPanelCtx }> - Panel component
  • label: string - Panel label
  • startOpen?: boolean - Whether panel starts expanded
  • placement?: [string, string] - Optional placement
  • rank?: number - Optional ordering
addSidebarPanel({
  id: 'metadata-panel',
  component: MetadataPanel,
  label: 'Metadata',
  startOpen: true,
});

addSidebar(config)

Add a full custom sidebar to item forms.

Config:

  • id: string - Unique sidebar ID
  • component: React.ComponentType<{ ctx: RenderItemFormSidebarCtx }> - Sidebar component
  • label: string - Sidebar label
  • preferredWidth?: number - Preferred width in pixels
  • shouldApply?: (model: ItemType, ctx: any) => boolean - Conditional display

Field Extensions

addFieldExtension(config)

Register a manual field extension (editor or addon).

Config:

  • id: string - Unique extension ID
  • name: string - Extension name
  • type: 'editor' | 'addon' - Extension type
  • fieldTypes: string[] - Compatible field types
  • component: React.ComponentType<{ ctx: RenderFieldExtensionCtx }> - Extension component
  • configurable?: boolean - Whether extension is configurable
  • configComponent?: React.ComponentType - Config screen component (if configurable)
  • validateConfig?: (params: Record<string, unknown>) => Record<string, string> - Config validator
addFieldExtension({
  id: 'color-picker',
  name: 'Color Picker',
  type: 'editor',
  fieldTypes: ['string'],
  component: ColorPickerEditor,
  configurable: true,
  configComponent: ColorPickerConfig,
});

overrideFieldExtension(config)

Automatically override field extensions based on conditions.

Config:

  • shouldApply: (field: Field, ctx: OverrideFieldExtensionsCtx) => boolean - Condition function
  • editor?: { id: string; parameters?: Record<string, unknown> } - Editor override
  • addons?: Array<{ id: string; parameters?: Record<string, unknown> }> - Addon overrides
overrideFieldExtension({
  shouldApply: (field, ctx) => field.attributes.api_key === 'color',
  editor: { id: 'color-picker' },
});

addHiddenField(predicate)

Conditionally hide fields from the DatoCMS UI based on custom logic.

Predicate:

  • (field: Field, ctx: OverrideFieldExtensionsCtx) => boolean - Function that returns true if the field should be hidden

Behavior:

  • Multiple predicates can be registered by calling addHiddenField multiple times
  • Hidden field predicates are checked before user-defined field extension overrides
  • When a predicate returns true, the field is automatically hidden from the UI
  • The field editor is replaced with a built-in hidden field extension that renders nothing

Example - Hide specific fields in a block:

import { addHiddenField, getFieldItemType } from '~/plugin';

addHiddenField((field, ctx) => {
  const hiddenFields = ['managed_id', 'managed_data'];
  const blockApiKey = 'external_content_block';

  const parent = getFieldItemType(field, ctx);
  return (
    parent &&
    parent.attributes.api_key === blockApiKey &&
    hiddenFields.includes(field.attributes.api_key)
  );
});

Example - Hide fields based on field type:

addHiddenField((field, ctx) => {
  // Hide all JSON fields with a specific prefix
  return (
    field.attributes.field_type === 'json' &&
    field.attributes.api_key.startsWith('internal_')
  );
});

Use cases:

  • Hide managed/internal fields that should not be edited manually
  • Conditionally hide fields based on parent model or block type
  • Hide fields used for automation or integration purposes
  • Dynamically hide fields based on complex business logic

UI Components

addModal(config)

Register a modal component.

Config:

  • id: string - Unique modal ID
  • component: React.ComponentType<{ ctx: RenderModalCtx }> - Modal component
addModal({
  id: 'confirmation-modal',
  component: ConfirmationModal,
});

configureConfigScreen(config)

Set the plugin settings screen.

Config:

  • component: React.ComponentType<{ ctx: RenderConfigScreenCtx }> - Settings screen component
configureConfigScreen({
  component: PluginSettingsScreen,
});

Actions

addDropdownAction(config)

Add dropdown actions to various contexts.

Config:

  • type: 'field' | 'itemForm' | 'items' | 'uploads' - Action context
  • id: string - Unique action ID
  • label: string - Action label
  • icon?: Icon - Optional icon
  • execute: (ctx: ExecuteCtx) => Promise<void> - Action handler
  • shouldApply?: (...args: any[]) => boolean - Conditional display
addDropdownAction({
  type: 'items',
  id: 'bulk-export',
  label: 'Export to CSV',
  icon: 'download',
  execute: async (ctx) => {
    // Export logic
  },
});

Structured Text Customizations

customBlockStylesForStructuredTextField(handler)

Add custom block styles to structured text fields (e.g., emphasized paragraphs, special headings).

Handler: (field: Field, ctx: CustomBlockStylesForStructuredTextFieldCtx) => StructuredTextCustomBlockStyle[] | undefined

customBlockStylesForStructuredTextField((field, ctx) => {
  // Only apply to specific fields
  if (field.attributes.api_key !== 'article_content') {
    return [];
  }

  return [
    {
      id: 'emphasized',
      node: 'paragraph',
      label: 'Emphasized',
      appliedStyle: {
        fontFamily: 'Georgia',
        fontStyle: 'italic',
        fontSize: '1.4em',
        lineHeight: '1.2',
      },
    },
    {
      id: 'callout',
      node: 'heading',
      label: 'Callout Heading',
      appliedStyle: {
        backgroundColor: '#f0f0f0',
        padding: '0.5em',
      },
    },
  ];
});

customMarksForStructuredTextField(handler)

Add custom marks to structured text fields (e.g., spoiler text, custom highlights).

Handler: (field: Field, ctx: CustomMarksForStructuredTextFieldCtx) => StructuredTextCustomMark[] | undefined

customMarksForStructuredTextField((field, ctx) => {
  return [
    {
      id: 'spoiler',
      label: 'Spoiler',
      icon: 'bomb',
      keyboardShortcut: 'mod+shift+s',
      appliedStyle: {
        backgroundColor: 'rgba(0, 0, 0, 0.8)',
        color: 'rgba(0, 0, 0, 0.8)',
      },
    },
    {
      id: 'keyboard',
      label: 'Keyboard',
      icon: 'keyboard',
      appliedStyle: {
        fontFamily: 'monospace',
        backgroundColor: '#f0f0f0',
        padding: '0.2em 0.4em',
        borderRadius: '3px',
      },
    },
  ];
});

Note: You're responsible for rendering these custom styles and marks on the frontend using DatoCMS's Structured Text libraries (React, Vue, etc.).

Events

onBoot(handler)

Register a plugin initialization handler.

Handler: (ctx: OnBootCtx) => void | Promise<void>

onBoot(async (ctx) => {
  console.log('Plugin initialized');
});

onBeforeItemUpsert(handler)

Register a pre-save hook.

Handler: (item: any, ctx: any) => boolean | Promise<boolean>

Returns true to allow save, false to prevent.

onBeforeItemUpsert(async (item, ctx) => {
  if (!item.title) {
    ctx.alert('Title is required');
    return false;
  }
  return true;
});

onBeforeItemsDestroy(handler)

Register a pre-delete hook.

Handler: (items: any[], ctx: any) => boolean | Promise<boolean>

onBeforeItemsDestroy(async (items, ctx) => {
  const confirmed = await ctx.openConfirm({
    title: 'Delete items?',
    content: `Delete ${items.length} items?`,
  });
  return confirmed;
});

onBeforeItemsPublish(handler)

Register a pre-publish hook.

Handler: (items: any[], ctx: any) => boolean | Promise<boolean>

onBeforeItemsUnpublish(handler)

Register a pre-unpublish hook.

Handler: (items: any[], ctx: any) => boolean | Promise<boolean>

Connect

connect()

Build the complete configuration and connect to the DatoCMS plugin SDK. This must be called after all registrations are complete.

import { connect } from '~/plugin';

connect();

TypeScript

All types from datocms-plugin-sdk pass through transparently. Import types directly from the SDK:

import type { Field, ItemType, RenderPageCtx } from 'datocms-plugin-sdk';

You can also import configuration types from this library:

import type {
  DropdownActionConfig,
  FieldExtensionConfig,
  FormOutletConfig,
  PageConfig,
} from '@oddcommon/datocms-plugin-kit';

Architecture

This library uses a factory function pattern that returns registration helpers. These helpers build up a shared configuration object internally. When you call connect(), the complete configuration is passed to the DatoCMS SDK's connect() function.

The hybrid approach allows you to:

  • Register both component and declaration in a single call (combined approach)
  • Split components and declarations across files
  • Organize your plugin code by feature rather than hook type

License

MIT

Author

Alban Creton - OddCommon

Disclaimer

The very first version of the library was almost entirely generated by Anthropic's LLM models Claude Sonnet 4.5 and Claude Opus 4.1 using Claude Code.

All the prompts used to generate that first version have been preserved in .claude. The design and plan files were generated using @obra's superpowers skills. The untouched generated code can be found in the very first commit made for this package.