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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@allejo/decap-extras

v0.0.10

Published

A TypeScript utility library for Decap CMS with utility types to derive types from Decap CMS configuration files.

Readme

@allejo/decap-extras

[!WARNING]

This is an experimental package in early development. I'm currently dog feeding this project in my own projects and am constantly improving the API as I discover better patterns.

See my nextjs-decap-typescript-template repository for a real-world example of how to use this package in a Next.js project.

A TypeScript utility library for Decap CMS that does two things:

  1. Widget factory functions — type-safe helpers for building Decap CMS field configuration objects instead of writing raw object literals by hand.
  2. Type inference utilities — TypeScript utility types that derive content types directly from your CMS config, so you never have to maintain separate type definitions for your CMS content.

Installation

npm install @allejo/decap-extras
# or
pnpm add @allejo/decap-extras
# or
yarn add @allejo/decap-extras

This package requires decap-cms-core as a peer dependency:

npm install decap-cms-core

Usage

Widget factories

All factory functions follow a similar signature of widget(label, name, options?).

import {
	boolWidget,
	imageWidget,
	INLINE_MARKDOWN,
	listWidget,
	markdownWidget,
	numberWidget,
	objectWidget,
	optional,
	RICH_MARKDOWN,
	selectWidget,
	stringWidget,
	textWidget,
} from '@allejo/decap-extras';

Widget modifiers

optional(widget)

Marks a widget field as optional, allowing it to be left empty in DecapCMS. This modifier can be applied to any widget type to make it non-required.

TypeScript:

optional(stringWidget('Middle Name', 'middleName'));

YAML Configuration:

- label: Middle Name
  name: middleName
  widget: string
  required: false # All fields are required by default, so this is necessary to make it optional

Data Store YAML:

middleName: # Can be left empty

The optional modifier can be used with all widget types including complex ones:

optional(
	objectWidget('Metadata', 'metadata', [stringWidget('Author', 'author')]),
);
optional(listWidget('Tags', 'tags', stringWidget('Tag', 'value')));

The optional modifier affects both the Decap CMS UI (field is not required) and the derived TypeScript type (field becomes T | undefined).

Learn more: DecapCMS Common Widget Options

Primitive widgets

stringWidget(label, name, opts?)

Creates a single-line text input field for short text content.

TypeScript:

stringWidget('Title', 'title');

YAML Configuration:

- label: Title
  name: title
  widget: string

Data Store YAML:

title: Welcome to My Blog

Unlike the textWidget which provides multi-line input, the string widget is designed for concise, single-line values like titles, slugs, or names.

optional(stringWidget('Slug', 'slug'));

See also: textWidget, markdownWidget Learn more: DecapCMS String Widget Docs

textWidget(label, name, opts?)

Creates a multi-line text input field for longer text content.

TypeScript:

textWidget('Description', 'description');

YAML Configuration:

- label: Description
  name: description
  widget: text

Data Store YAML:

description: |-
  This is a longer description that spans
  multiple lines in the data store.

Unlike the stringWidget which provides single-line input, the text widget is designed for paragraphs and multi-line content.

optional(textWidget('Notes', 'notes'));

See also: stringWidget, markdownWidget Learn more: DecapCMS Text Widget Docs

numberWidget(label, name, opts?)

Creates a numeric input field with optional min, max, and step constraints.

TypeScript:

numberWidget('Priority', 'priority');

YAML Configuration:

- label: Priority
  name: priority
  widget: number

Data Store YAML:

priority: 1

The number widget provides numeric input capabilities:

optional(numberWidget('Views', 'views'));

Learn more: DecapCMS Number Widget Docs

boolWidget(label, name, opts?)

Creates a checkbox input field for boolean values.

TypeScript:

boolWidget('Published', 'published');

YAML Configuration:

- label: Published
  name: published
  widget: boolean

Data Store YAML:

published: true

The boolean widget creates a checkbox input in the editor:

optional(boolWidget('Draft', 'isDraft'));

Learn more: DecapCMS Boolean Widget Docs

selectWidget(label, name, choices, opts?)

Creates a dropdown menu with predefined options.

TypeScript:

selectWidget('Status', 'status', ['draft', 'published', 'archived'] as const);

YAML Configuration:

- label: Status
  name: status
  widget: select
  options:
    - draft
    - published
    - archived

Data Store YAML:

status: published

Pass the choices array as const to get a narrowed literal union type; omit as const to get string:

const themeField = selectWidget('Theme', 'theme', ['light', 'dark'] as const);
// TypeScript type: "light" | "dark"

const looseField = selectWidget('Size', 'size', ['sm', 'md', 'lg']);
// TypeScript type: string

The select widget can be made optional:

optional(
	selectWidget('Category', 'category', [
		'tech',
		'lifestyle',
		'business',
	] as const),
);

Learn more: DecapCMS Select Widget Docs

imageWidget(label, name, opts?)

Creates a file upload field with image preview capabilities.

TypeScript:

imageWidget('Featured Image', 'featuredImage');

YAML Configuration:

- label: Featured Image
  name: featuredImage
  widget: image

Data Store YAML:

featuredImage: /images/hero.jpg

The image widget provides file upload functionality with image preview. To pass a default transformation (supported by services like Cloudinary), use the opts parameter to pass a media_library configuration:

imageWidget('Hero Image', 'hero');
optional(imageWidget('Avatar', 'avatar'));

Learn more: DecapCMS Image Widget Docs

Complex widgets

objectWidget(label, name, fields, opts?)

Groups multiple fields together as a nested object in the data store.

TypeScript:

objectWidget('Author', 'author', [
	stringWidget('Name', 'name'),
	stringWidget('Email', 'email'),
]);

YAML Configuration:

- label: Author
  name: author
  widget: object
  fields:
    - label: Name
      name: name
      widget: string
    - label: Email
      name: email
      widget: string

Data Store YAML:

author:
  name: Jane Doe
  email: [email protected]

This is useful for creating structured data with related properties. Objects can be nested within lists or other objects to create complex data structures:

optional(
	objectWidget('Metadata', 'metadata', [
		stringWidget('SEO Title', 'seoTitle'),
		textWidget('Description', 'description'),
	]),
);

Learn more: DecapCMS Object Widget Docs

listWidget(label, name, field/fields, opts?)

Creates a list widget that can contain either single-field items or multi-field items.

Single-field list:

TypeScript:

listWidget('Tags', 'tags', stringWidget('Tag', 'value'));

YAML Configuration:

- label: Tags
  name: tags
  widget: list
  field:
    label: Tag
    name: value
    widget: string

Data Store YAML:

tags:
  - value: React
  - value: TypeScript

Multi-field list:

TypeScript:

listWidget('Authors', 'authors', [
	stringWidget('Name', 'name'),
	imageWidget('Avatar', 'avatar'),
]);

YAML Configuration:

- label: Authors
  name: authors
  widget: list
  fields:
    - label: Name
      name: name
      widget: string
    - label: Avatar
      name: avatar
      widget: image

Data Store YAML:

authors:
  - name: Jane Doe
    avatar: /images/jane.jpg
  - name: John Smith
    avatar: /images/john.jpg

[!WARNING]

When passing an Object widget (e.g., objectWidget(...)), this function will unwrap the object and return Array<{ ... }>, not Array<T>. This is done as a convenience so you don't have an extra layer of nesting in your data structure. For example, by unwrapping the object, you can have a list of items like this:

listOfItems:
  - label: 'Item 1'
    value: 'Value 1'

If the object was not unwrapped, the data structure would look like this:

listOfItems:
  - item:
      label: 'Item 1'
      value: 'Value 1'

If you do want the extra nesting, simply wrap the object in an array when passing it to listWidget(), like this:

const nestedField = listWidget('Nested Items', 'nestedItems', [
	objectWidget('Item', 'item', [
		stringWidget('Label', 'label'),
		stringWidget('Value', 'value'),
	]),
]);

The list widget can be made optional:

optional(listWidget('Tags', 'tags', stringWidget('Tag', 'value')));

Learn more: DecapCMS List Widget Docs

Markdown widget

markdownWidget(label, name, opts?)

Creates a rich text editor for markdown content.

TypeScript:

// Full markdown editor
markdownWidget('Content', 'content');

// Restricted to bold, italic, lists, links, and quotes — no images or code blocks
markdownWidget('Description', 'description', RICH_MARKDOWN);

// Inline elements only: bold, italic, and links
markdownWidget('Caption', 'caption', INLINE_MARKDOWN);

YAML Configuration:

- label: Content
  name: content
  widget: richtext

Data Store YAML:

content: |
  # Heading
  This is **bold** and *italic* text with [links](https://example.com).

Learn more: DecapCMS RichText Widget Docs

Common options

Every factory accepts an optional last argument for common widget options:

const slugField = stringWidget('Slug', 'slug', {
	required: true,
	hint: 'URL-friendly identifier, e.g. my-page-title',
	pattern: ['^[a-z0-9-]+$', 'Lowercase letters, numbers, and hyphens only'],
});

Type inference

Define your CMS config using the widget factories and as const, then let PropsByCollectionAndFile derive a fully typed content object from it automatically.

// cms/config.ts
import {
	boolWidget,
	imageWidget,
	markdownWidget,
	objectWidget,
	optional,
	RICH_MARKDOWN,
	selectWidget,
	stringWidget,
} from '@allejo/decap-extras';

export const config = {
	collections: [
		{
			label: 'Pages',
			name: 'pages' as const,
			files: [
				{
					label: 'Home',
					name: 'home' as const,
					file: 'content/home.yml',
					fields: [
						stringWidget('Title', 'title'),
						optional(stringWidget('Subtitle', 'subtitle')),
						imageWidget('Hero Image', 'hero'),
						markdownWidget('Body', 'body', RICH_MARKDOWN),
						selectWidget('Theme', 'theme', ['light', 'dark'] as const),
						objectWidget('SEO', 'seo', [
							stringWidget('Meta Title', 'metaTitle'),
							optional(stringWidget('Meta Description', 'metaDescription')),
						]),
					],
				},
			],
		},
	],
} as const;
// components/HomePage.ts
import type { PropsByCollectionAndFile } from '@allejo/decap-extras';

import type { config } from '@/cms/config';

type HomePageProps = PropsByCollectionAndFile<typeof config, 'pages', 'home'>;
// Resolves to:
// {
//   title: string;
//   subtitle?: string;
//   hero: string;
//   body: string;
//   theme: "light" | "dark";
//   seo: {
//     metaTitle: string;
//     metaDescription?: string;
//   };
// }

function render(page: HomePageProps) {
	console.log(page.title); // string
	console.log(page.subtitle); // string | undefined
	console.log(page.theme); // "light" | "dark"
	console.log(page.seo.metaTitle); // string
}

Typing reusable widget functions

Use WidgetTypeFromFactory to derive a TypeScript type from a widget factory function without duplicating the definition.

import type { WidgetTypeFromFactory } from '@allejo/decap-extras';
import { objectWidget, optional, stringWidget } from '@allejo/decap-extras';

function seoWidget() {
	return objectWidget('SEO', 'seo', [
		stringWidget('Meta Title', 'metaTitle'),
		optional(stringWidget('Meta Description', 'metaDescription')),
	]);
}

type SeoWidget = WidgetTypeFromFactory<typeof seoWidget>;
// { metaTitle: string; metaDescription?: string }

Constraining to valid file names

Use CollectionItemNames to get a union of all file name literals for a collection. This is useful for constraining generic components or functions that accept a page name.

import type { CollectionItemNames } from '@allejo/decap-extras';

import type { config } from '@/cms/config';

type PageName = CollectionItemNames<typeof config, 'pages'>;
// "home" | "about" | ...

function getPageProps<F extends PageName>(
	page: F,
): PropsByCollectionAndFile<typeof config, 'pages', F> {
	// ...
}

React Integration

Decap CMS is distributed either as a single .js bundle you can load in an HTML file with <script> or as an ESM module you can import. In order to integrate type-safety into working with Decap, we want to incorporate Decap CMS as a Next.js page so that we can get all the type-safety from working within it as a framework. There's one small problem with this though...

You can also use Decap CMS as an npm module. Wherever you import Decap CMS, it automatically runs, taking over the current page.

This means, we can't just import Decap CMS in a Next.js page and call it a day. This is because at build time, Next.js will run the code in the page to generate the static HTML, and if Decap CMS is imported and initialized at that time, it's going to try to take over the page during the build process, which is not what we want. We only want Decap CMS to take over the page when it's running in the browser.

To work around this, we can use dynamic imports (i.e., import()) inside a useEffect hook to initialize Decap CMS only on the client after the Next.js boilerplate has rendered and allow the CMS instance to take over the page.

useEffect(() => {
	Promise.all([
		import('decap-cms-app'),

		// You can import other Decap CMS modules
		// import('decap-cms-media-library-cloudinary')
	]).then(([cmsMod /*, cloudinaryMod*/]) => {
		const cms = cmsMod.default;
		const cloudinary = cloudinaryMod.default;

		cms.init({ config });

		// ...
		// cms.registerPreviewStyle(...)
		// cms.registerPreviewTemplate(...);
		// cms.registerWidget(...);
		// cms.registerMediaLibrary(cloudinary);
	});
}, []);

The import() syntax, commonly called dynamic import, is a function-like expression that allows loading an ECMAScript module asynchronously and dynamically into a potentially non-module environment.

Is there a more elegant solution to this problem? I'm not sure! If you can think of one, I would gladly welcome the idea.

But knowing all of this and setting it up gets tedious and boilerplate-y really fast. So, in this package, we export a React component called DecapInstance that abstracts away all of this dynamic importing and initialization logic, so you can just use it as a wrapper around your admin page and pass in your config, preview styles, templates, widgets, and Cloudinary flag as props.

The <DecapInstance /> Component

We provide a React component called DecapInstance that handles the dynamic import and initialization of Decap CMS for you.

import type { PropsByCollectionAndFile } from '@allejo/decap-extras';
import { getCssFilesFromManifest } from '@allejo/decap-extras/next';
import { DecapInstance } from '@allejo/decap-extras/react';
import type { ComponentType } from 'react';

import type { cmsConfig } from '@/cms/config';

// The HomePageProps type and HomePreview component are normally imported from
// other files, but are included here inline for demonstration purposes.
type HomePageProps = PropsByCollectionAndFile<
	typeof cmsConfig,
	'pages',
	'home'
>;
function HomePreview(props: HomePageProps) {
	return (
		<article>
			<h1>{props.title}</h1>
			{props.subtitle ? <p>{props.subtitle}</p> : null}
		</article>
	);
}

type AdminProps = {
	cssFiles: string[];
};

export async function getStaticProps() {
	const css = getCssFilesFromManifest(process.env.NODE_ENV ?? 'development', [
		'/',
		'/_app',
	]);

	return {
		props: {
			cssFiles: css.flattenedCssFiles,
		},
	};
}

export default function Admin({ cssFiles }: AdminProps) {
	return (
		<DecapInstance
			config={cmsConfig}
			cssFiles={cssFiles}
			// Pass explicitly so media-library behavior is visible in admin setup.
			useCloudinary={true}
			pages={{
				home: HomePreview as unknown as ComponentType<never>,
			}}
			widgets={{}}
			onInit={(cms) => {
				console.log('Decap initialized', cms);
			}}
		/>
	);
}

How it Works

getCssFilesFromManifest reads Next.js's .next/build-manifest.json at build time and returns all CSS files associated with the / and /_app routes. These are passed as props to the admin page so that we can register the CSS files into Decap's preview functionality.

export async function getStaticProps() {
	const cssFiles = getCssFilesFromManifest(process.env.NODE_ENV, [
		'/',
		'/_app',
	]);
	return { props: { cssFiles: cssFiles['flattenedCssFiles'] } };
}

[!WARNING]

This is a bit of a hacky solution to get the CSS files and therefore has some known issues.

The CSS for a page might not exist during local development because Next.js only generates the build manifest as the pages are requested. Therefore, if launch next dev and immediately open the admin page, the CSS files won't be found for the homepage (i.e., /) and won't be registered in the CMS preview, resulting in an unstyled preview. To work around this, you can either visit the homepage first to trigger the generation of the CSS files in the manifest, or run next build to generate the manifest with all CSS files before running next dev. During local development, this function will check the dev manifest at .next/dev/build-manifest.json first, and fallback to the regular manifest at .next/build-manifest.json if the dev manifest doesn't exist, so it should work in either case as long as the CSS files have been recorded.

If you have a better idea of handling this, please reach out!

Initialization sequence inside useEffect

cms.init({ config });

Bootstraps the CMS with the config from src/cms/config.ts.

cssFiles.forEach((css) => cms.registerPreviewStyle(css));

Registers every collected CSS file with the CMS preview panel so the editor renders with production styles.

[!WARNING]

FIXME: There HAS to be a better way of handling this so that we only load the relevant stylesheets for specific previews. I'm not sure how to solve this yet, but I'll come back to it.

cms.registerPreviewTemplate(pageId, ({ entry }) => {
  return <Component {...entry.get('data').toJSON()} />;
});

Maps each CMS page entry (home, etc.) to its real Next.js page component, passing the editor's live data as props. Editors see the actual production component rendering their changes in real time.

cms.registerMediaLibrary(cloudinary);

Registers the Cloudinary media picker.

[!WARNING]

Being able to disable Cloudinary is a work-in-progress. I've always needed it for the projects I'm building so it's enabled by default. I'll get to respecting the "disable" environment variable... eventually.

Next.js utilities

A server-side utility for getting CSS from Next.js build manifests. This is useful for registering CSS stylesheets into Decap's CMS preview system.

import { getCssFilesFromManifest } from '@allejo/decap-extras/next';

Use getCssFilesFromManifest in getStaticProps to collect the CSS paths needed by your page and pass them as props:

export async function getStaticProps() {
	const cssFiles = getCssFilesFromManifest(process.env.NODE_ENV, [
		'/',
		'/_app',
	]);

	return {
		props: {
			cssFiles: cssFiles['flattenedCssFiles'],
		},
	};
}

Then register each path as a preview stylesheet in your Decap CMS admin component:

export default function Admin({ cssFiles }: Props) {
	// ...

	cssFiles.forEach((css) => {
		cms.registerPreviewStyle(css);
	});
}

API reference

Widget API reference

Widget modifiers

| Function | Description | | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | | optional(widget) | Marks a widget field as optional (non-required). Learn more |

Primitive widgets

| Function | Decap widget | TypeScript type | | ------------------------------------------- | ------------------------------------------------------- | ------------------------- | | stringWidget(label, name, opts?) | string | string | | textWidget(label, name, opts?) | text | string | | numberWidget(label, name, opts?) | number | number | | boolWidget(label, name, opts?) | boolean | boolean | | selectWidget(label, name, choices, opts?) | select | literal union or string | | imageWidget(label, name, opts?) | image | string |

Complex widgets

| Function | Decap widget | TypeScript type | | -------------------------------------------- | ----------------------------------------------------- | ---------------- | | listWidget(label, name, field, opts?) | list | Array<T> | | listWidget(label, name, fields[], opts?) | list | Array<{ ... }> | | objectWidget(label, name, fields[], opts?) | object | { ... } |

Rich text widgets

| Function | Decap widget | TypeScript type | | ------------------------------------ | -------------------------------------------------------------- | --------------- | | markdownWidget(label, name, opts?) | richtext | string |

Markdown presets

| Constant | Description | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | RICH_MARKDOWN | Stripped down markdown editor with bold, italic, lists (bullet + numbered), quotes, and links. No images or code blocks. Rich Text preview only. | | INLINE_MARKDOWN | Stripped down markdown widget that allows only inline elements (bold, italic, and links). No block-level elements like lists or quotes. Rich Text preview only. |

Type utilities

| Type | Description | | ----------------------------------------- | ------------------------------------------------------------------------------------ | | PropsByCollectionAndFile<TConfig, C, F> | Derives a typed content object from a CMS config for collection C and file F. | | CollectionItemNames<TConfig, C> | Union of all file name literals defined in collection C. | | WidgetTypeFromFactory<F> | Returns the TypeScript content type produced by a widget factory function F. | | OptionalWidget<T> | Brands a CmsField as optional (added by optional()). | | WidgetOpts<T> | The options type for a given widget field type, with widget and fields stripped. |

Server utilities

| Function | Description | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | getCssFilesFromManifest(env, pages) | Reads the Next.js build manifest and returns CSS paths for the given page routes, grouped by page (allCssFiles) and as a deduplicated flat list (flattenedCssFiles). |

React utilities

| Type/Function | Description | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | DecapInstance | React component that initializes Decap CMS, registers preview CSS/templates/widgets, optionally enables Cloudinary, and returns null after setup. | | DecapInstanceProps | Props contract for DecapInstance, including config, preview CSS paths, page template registry, widget registry, and optional onInit/useCloudinary. |

License

MIT