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

@noteblob/plugin-sdk

v0.1.6

Published

Authoring SDK for NoteBlob preview plugins.

Readme

@noteblob/plugin-sdk

Authoring SDK for NoteBlob plugins. This package is the TypeScript contract the JS-based plugins program against; the host runtime is provided by the app.

Taxonomy

A plugin declares an extension.id: the extension point it implements.

| extension.id | triggered by | JS? | interface | | ------------------- | --------------------------------- | ------ | ------------------------------ | | note-preview.node | markdown node target (manifest) | yes | NotePreviewNodePlugin<Token> | | theme | user selection | no | — (JSON color/font) |

A package (installed from a URL) contains one or more plugins, possibly of mixed extension points. extension.id is the seam for future surfaces (editor.action, …) with no manifest-format change.

The contract

A JS plugin is a render plus two optional hooks — onPrepare and onThemeChange:

interface PreviewContext {
  theme: ThemeInfo;
  assets: AssetLoader;
}

interface NotePreviewNodePlugin<Token = CodeNode> {
  onPrepare?(ctx: PreviewContext): void | Promise<void>; // once: load libs/styles
  render(
    node: PreviewNode<Token>,
    ctx: PreviewContext,
  ): void | Cleanup | Promise<void | Cleanup>;
  onThemeChange?(
    node: PreviewNode<Token>,
    ctx: PreviewContext,
  ): void | Promise<void>;
}
  • onPrepare runs once, before any render — load your libraries/styles.
  • render runs per node, after the host has placed its element in the DOM. It gets { el, token } (the element + the parsed token for that node kind), may be sync or async, and may return a Cleanup to release per-node resources.
  • Theme is CSS-first. The host owns the :root custom properties and the data-color-scheme attribute and swaps them on every theme change — style your output with var(--…) and it re-themes with zero JS and no re-render.
  • onThemeChange is only for output CSS can't restyle (canvas/SVG). The host calls it per mounted node after swapping the variables; repaint in place — the node is not re-rendered and keeps its state.
  • Load deps via ctx.assets.loadLibrary(relPath) — it returns the lib; hold it in a module variable (the host instantiates your plugin per document).

note-preview / node

The manifest selects the markdown node kind. The plugin type is generic because the token depends on that manifest variant: code-block plugins receive CodeNode, heading plugins receive HeadingNode, and future node variants add their own token shape.

import type { NotePreviewNodePlugin, PreviewNode } from "@noteblob/plugin-sdk";

interface Mermaid {
  initialize(c: object): void;
  run(c: { nodes: ArrayLike<HTMLElement> }): Promise<void>;
}
const diagramBackground = "#ffffff";
let mermaid: Mermaid;

async function draw({ el, token }: PreviewNode) {
  mermaid.initialize({
    startOnLoad: false,
    securityLevel: "strict",
    theme: "default",
    fontFamily: getComputedStyle(el).fontFamily,
    flowchart: { useMaxWidth: false },
    themeVariables: {
      background: diagramBackground,
      fontFamily: getComputedStyle(el).fontFamily,
    },
  });
  el.classList.add("mermaid");
  el.removeAttribute("data-processed");
  el.textContent = token.source;
  await mermaid.run({ nodes: [el] });
  normalizeSVG(el);
}

function normalizeSVG(el: HTMLElement) {
  const svg = el.querySelector("svg");
  const width = svg
    ?.getAttribute("viewBox")
    ?.split(/[\s,]+/)
    .map(Number)[2];
  if (!svg) return;
  if (width && Number.isFinite(width))
    svg.style.width = `${Math.ceil(width)}px`;
  svg.style.maxWidth = "100%";
  svg.style.height = "auto";
  svg.style.backgroundColor = diagramBackground;
}

export default {
  async onPrepare({ assets }) {
    mermaid = await assets.loadLibrary<Mermaid>("vendor/mermaid.js");
  },
  render: draw,
} satisfies NotePreviewNodePlugin;

For a heading plugin:

import type { HeadingNode, NotePreviewNodePlugin } from "@noteblob/plugin-sdk";

export default {
  render({ el, token }) {
    el.dataset.level = String(token.level);
  },
} satisfies NotePreviewNodePlugin<HeadingNode>;

Stateful plugins return a Cleanup (runs before the next render / on removal):

render({ el, token }, { theme }) {
  const chart = new Chart(el.appendChild(el.ownerDocument.createElement("canvas")), parse(token, theme));
  return () => chart.destroy();
}

The host owns node discovery and hands you each el — you never need to query the document to find the nodes your manifest targets.

theme

Themes are not JS — JSON files defining color/font, loaded by the app's native theme system. A theme plugin needs no entry, no SDK type; its source directory contains one or more theme JSON files matching the exported noteblob-theme.schema.json schema.

Manifest

{
  "apiVersion": "1",
  "id": "com.acme.pack",
  "name": "Acme Pack",
  "version": "1.0.0",
  "plugins": [
    {
      "id": "mermaid",
      "source": "mermaid",
      "extension": {
        "id": "note-preview.node",
        "entry": "main.js",
        "node": "codeBlock",
        "languages": ["mermaid"],
      },
    },
    {
      "id": "heading-tools",
      "source": "heading-tools",
      "extension": {
        "id": "note-preview.node",
        "entry": "main.js",
        "node": "heading",
        "levels": [1, 2],
      },
    },
    { "id": "themes", "source": "themes", "extension": { "id": "theme" } },
  ],
}

The trigger lives only in the manifest (node + node-specific filters), never in code.

noteblob-package.json is the single source of truth and is read directly by the app. The SDK publishes noteblob-package.schema.json for editor/CI validation; it is generated from the SDK's TypeScript manifest types.

Rules & guarantees

  • One plugin = one extension. Need two behaviors? Ship two plugins.
  • No network. Preview webviews block egress (CSP + navigation policy), so a plugin can't exfiltrate content. Bundle heavy deps and pull them in via assets.loadLibrary — keep them out of your main.js bundle.
  • Graceful degradation. The host shows the default node output until render runs; if render fails or a node has no plugin, it stays default markdown output. The document never breaks.
  • Conflicts (two plugins claiming the same node filter) are detected from manifests at install time.

Building a plugin

The SDK is imported with import type only, so it's erased at compile — your main.js carries no dependency on @noteblob/plugin-sdk:

esbuild main.ts --bundle --format=esm --target=es2020 --outfile=main.js

Ship the package as noteblob-package.json + each plugin's source directory.

Validating a plugin package

After building your plugin entrypoints, validate the installable package:

npx noteblob-plugin validate .

The validator checks noteblob-package.json against the SDK schema, verifies declared source directories and source-relative entry files, validates theme JSON files against noteblob-theme.schema.json, imports each JS plugin entrypoint, checks the plugin export shape, and runs onPrepare with a mock AssetLoader so loadLibrary, loadStyle, and assetURL references point at real files inside the plugin's source directory. Because onPrepare declares runtime libraries, styles, and asset URLs, it is part of the package validation pass.

SDK build (this repo)

npm install
npm test
npm run build              # tsc -> dist/ (.d.ts, types only)
npm run generate:schema    # updates noteblob-package.schema.json and noteblob-theme.schema.json

Official plugins live in ../noteblob-plugins. They are a separate npm project and depend on the published @noteblob/plugin-sdk package.