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

@csirt-cms/widget-core

v0.1.1

Published

Framework-agnostic widget framework for the Central Content Management Service: registry, schemas, defaults, visibility, migrations and Public API client.

Downloads

306

Readme

@csirt-cms/widget-core

The framework-agnostic half of the CSIRT CMS widget system: widget definitions, the registry, validation, defaults, visibility rules, migrations, import/export, the Public API client and theming.

It contains no UI code. The React and Vue packages both build on it, which is why a widget's schema, defaults and version are written once and behave identically everywhere.

npm install @csirt-cms/widget-core

zod is a dependency; the framework packages declare it as a peer so one copy is shared with your CMS admin app.


The Public API client

The CMS is headless. GET /api/v1/public/sites/{slug}/page/{pageSlug} returns a page as { site, page, seo, sections }, where each section is a { component, props } pair — the service never renders HTML.

import { createCmsClient } from "@csirt-cms/widget-core";

const client = createCmsClient({
  baseUrl: "https://cms.jogjaprov.go.id",
  site: "csirt",
});

const page = await client.getPage("beranda");
const nav = await client.getNavigation();
const announcements = await client.getAnnouncements();
const theme = await client.getTheme();

| Method | Endpoint | | --- | --- | | getSite() | /api/v1/public/sites/{slug} | | getPage(slug) | /api/v1/public/sites/{slug}/page/{pageSlug} | | getHomepage() | resolves homepageSlug, then fetches that page | | getNavigation() | /api/v1/public/sites/{slug}/navigation | | getAnnouncements() | /api/v1/public/sites/{slug}/announcement | | getTheme() | /api/v1/public/sites/{slug}/theme | | mediaUrl(objectKey) | /api/v1/public/media/object/{objectKey} |

The public surface takes no auth, so the client sends no credentials and is safe to call from a browser or a server.

Caching. Responses carry ETag and Cache-Control. Pass framework hints through requestInit:

const page = await client.getPage("beranda", { next: { revalidate: 60 } });

Errors. Every non-2xx throws CmsApiError carrying the service's error envelope. getPageOrNull() returns null on a 404 instead, for routes that render their own not-found state.


Widget definitions

A WidgetDefinition is everything the CMS needs to describe, validate, default and version a widget — with no components attached:

import { faqDefinition, builtInDefinitions } from "@csirt-cms/widget-core";

faqDefinition.key;      // "faq"
faqDefinition.category; // "Content"
faqDefinition.version;  // "1.0.0"
faqDefinition.schema;   // ZodType<FaqProps>
faqDefinition.defaults; // FaqProps

Ten widgets ship with the package: hero-carousel, feature-grid, faq, statistics, gallery, announcement, contact, timeline, html, markdown.

Props are hand-written interfaces

Each widget exports an explicit interface, with the schema pinned to it by satisfies:

export interface FaqProps { /* … */ }

export const faqSchema = z.object({ /* … */ }) satisfies z.ZodType<FaqProps>;

Not type FaqProps = z.infer<typeof faqSchema>, for two reasons. Vue's SFC compiler resolves types itself, separately from TypeScript, to turn defineProps<FaqProps>() into runtime prop declarations — it can follow a plain interface across a package boundary but not Zod's inferred conditional types. And named interfaces read far better than z.infer blobs in editor tooltips. The satisfies clause is what stops the two drifting.


Registry

The registry is generic over the component type, which is what lets React and Vue share it:

import { createWidgetRegistry } from "@csirt-cms/widget-core";

const registry = createWidgetRegistry<MyComponentType>();
registry.register(widget);

registry.get("faq");
registry.all();
registry.byCategory("Content");
registry.categories();      // populated categories, canonical order
registry.search("carousel"); // name, key, description, category, tags

Registering the same key at a different version throws rather than silently replacing it.


Validation and defaults

import { validateProps, withDefaults, safeProps } from "@csirt-cms/widget-core";

// Returns issues instead of throwing, so an editor can mark the bad fields.
const result = validateProps(faqDefinition, props);
if (!result.success) console.log(result.issues); // [{ path: "items.0.question", … }]

// Fill gaps from defaults.
const complete = withDefaults(faqDefinition, { title: "Help" });

// Never throws: unusable props fall back to defaults, so one malformed
// section degrades to its empty state instead of taking the page down.
const { props: safe, issues } = safeProps(faqDefinition, fromDatabase);

Registering widgets with the CMS

The Admin API stores each component's props schema as JSON Schema, not Zod:

import { toComponentRegistration } from "@csirt-cms/widget-core";

await fetch(`${baseUrl}/api/v1/components`, {
  method: "POST",
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
  body: JSON.stringify(toComponentRegistration(faqDefinition)),
});

This keeps the server-side component catalogue in step with the code.


Visibility

Mirrors SectionVisibilityDtohidden, devices, startAt, endAt — plus section status:

import { visibleSections } from "@csirt-cms/widget-core";

const renderable = visibleSections(sections, {
  device: "mobile",
  preview: false, // true also renders DRAFT and SCHEDULED, for the admin preview
  now: new Date(),
});

SCHEDULED sections are treated as publishable and left for the date checks to include or exclude.


Migrations

Bump a widget's version and add a matching migration; stored props are walked up to the current version at render time:

export const heroCarouselDefinition = {
  version: "2.0.0",
  migrations: [
    { from: "1.0.0", to: "2.0.0", migrate: (props) => ({ ...props, slides: props.items }) },
  ],
};

Props with no recorded version are assumed current — a widget that has never had a breaking change needs no migrations, and guessing otherwise would corrupt good data.


Import / export

import {
  exportSectionsToJson, importSections, duplicateSection, moveSection, toReorderPayload,
} from "@csirt-cms/widget-core";

const json = exportSectionsToJson(sections);
const imported = importSections(json); // DRAFT by default; nothing goes live by accident

const copy = duplicateSection(section);
const reordered = moveSection(sections, id, -1); // clamps at the edges

// Body for PUT /api/v1/pages/{pageId}/sections/reorder
await put(toReorderPayload(reordered));

Theming

GET /api/v1/public/sites/{slug}/theme returns open token objects. Flatten them into CSS custom properties:

import { themeToCssText, themeToCssVars, applyTheme } from "@csirt-cms/widget-core";

themeToCssVars({ colors: { primaryHover: "#111", brand: { 500: "#0af" } } });
// { "--cms-color-primary-hover": "#111", "--cms-color-brand-500": "#0af" }

Inline themeToCssText(theme) in a <style> tag during the server render. applyTheme() writes the same values after mount, which is simpler but flashes the stylesheet defaults first.


HTML and Markdown

import { sanitizeHtml, renderMarkdown } from "@csirt-cms/widget-core";

sanitizeHtml('<p onclick="x()">hi</p><script>alert(1)</script>'); // "<p>hi</p>"
renderMarkdown("## Title\n\nSome **bold** text.");

Both run identically in Node and the browser, so server-rendered and client-rendered markup match.

Choose the right sanitizer for your threat model. sanitizeHtml is an allowlist string tokenizer, not a spec-compliant HTML parser. It strips script and style elements and their contents, event-handler attributes, javascript: URLs, and everything outside the allowlist — the right trade-off for content authored by trusted CMS editors, which is what this package is built for. If widget props can carry input from untrusted or public sources, pass a battle-tested sanitizer instead via the sanitizer prop on the html widget:

import DOMPurify from "isomorphic-dompurify";
<HtmlRenderer {...props} sanitizer={(html) => DOMPurify.sanitize(html)} />

renderMarkdown covers the subset CMS editors write: headings, emphasis, links, images, lists, blockquotes, fenced and inline code, tables and horizontal rules. Raw HTML inside Markdown is escaped to literal text. It is deliberately not a CommonMark implementation — it ships zero dependencies.


License

MIT