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

@kirigami/sdk

v0.3.1

Published

Shared runtime for Kirigami plugins: hooks, commands, custom task types, and cache.

Readme


@kirigami/sdk

Shared runtime for Kirigami plugins — hooks, commands, task types, and the on-disk cache.

npm version License: GPL-3.0-or-later Node.js >=24.0.0 Website


Overview

@kirigami/sdk is the shared runtime between @kirigami/kirigami (kirigami-core) and plugin packages: essentially, an in-memory hook registry.

@kirigami/kirigami fires named hooks during its build; a plugin hooks into them via on(hookName, fn). Since both depend on the same instance of this module (via the monorepo's npm workspaces, or as a regular dependency once published), they share the same in-memory registry — the plugin doesn't need to know anything about kirigami-core's internal structure. Core and plugins must resolve the same installed SDK module: separate physical copies or versions have separate registries; npm dependency declarations alone do not guarantee a shared instance.

Part of the Kirigami project ecosystem.


Table of contents


What's new in 0.3.0

  • registerTaskType() — a plugin contributes a new task type, resolved by the engine alongside the built-in ones.
  • registerCommand() — a plugin adds a kiri <name> subcommand; also getCommand() and listCommands().
  • scripts:register, tasks:register and commands:register hooks — a plugin ships runnable PHP scripts (optionally triggered at a build or export checkpoint), build tasks, and commands, the plugin counterparts of a project's own scripts: and tasks: entries. See Available hooks.
  • Copies of this package share one registry. npm installs a second copy when a plugin pins another SDK version than the engine; from 0.3.0 on, both copies see the same hooks, commands and task types.
  • These need @kirigami/kirigami 3.0.0.

What's new in 0.2.1

  • homepage + README pointed at the site (metadata only).

What's new in 0.2.0

  • esbuild:before / esbuild:after / esbuild:plugins hooks — the esbuild task now has the same extension points as sass, so a plugin can inject client-side JavaScript.
  • prepros:html hook — transform the final HTML of each rendered page; prepros:php hook — contribute a PHP file to the prepros runtime (register authoring tags / hooks from PHP). Both used by @kirigami/plugin-highlight.
  • runWaterfall() — pipe a value through listeners (vs. run(), which collects), and has() — check whether a hook has any listener.

Installation

npm install @kirigami/sdk

Usage in a plugin

import { on, HOOKS } from '@kirigami/sdk';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const pluginDir = path.dirname(fileURLToPath(import.meta.url));

export default function register() {
on(HOOKS.SASS_BEFORE, () => path.join(pluginDir, 'styles/before.scss'));
on(HOOKS.SASS_AFTER,  () => path.join(pluginDir, 'styles/after.scss'));

on(HOOKS.SASS_FUNCTIONS, () => ({
	'my-plugin-function($value)': (args) => {
		// ...
	},
}));

// Inject a client-side script into the first esbuild bundle.
on(HOOKS.ESBUILD_AFTER, () => path.join(pluginDir, 'client/init.js'));

// Rewrite the rendered HTML of every page (waterfall — return the new string).
on(HOOKS.PREPROS_HTML, (html, { file }) => html.replaceAll('<table>', '<table class="striped">'));
}

A listener can return:

  • a single value,
  • an array of values (automatically flattened into the result),
  • null/undefined to contribute nothing this run — useful if the listener wants to inspect the received context (see below) and decide to opt out.

It can also be async — run() awaits each listener before moving on to the next one.


Available hooks

Sass and esbuild hooks receive one argument, hookContext, shaped as { __root, task, exportPath, config } (the same values build() receives for the current task, plus the resolved kirigami.yaml config). The two prepros hooks use the separate signatures shown in the table.

| Hook | Task | Fired with | Expected return value | |---|---|---|---| | HOOKS.SASS_BEFORE | sass | hookContext | .scss file path(s), compiled before the entry | | HOOKS.SASS_AFTER | sass | hookContext | .scss file path(s), compiled after the entry | | HOOKS.SASS_FUNCTIONS | sass | hookContext | object(s) { 'signature($arg)': (args) => SassValue }, in the same shape as the Sass API's functions option | | HOOKS.ESBUILD_BEFORE | esbuild | hookContext | .js/.ts file path(s), bundled (as side-effect imports) before the entry | | HOOKS.ESBUILD_AFTER | esbuild | hookContext | .js/.ts file path(s), bundled (as side-effect imports) after the entry | | HOOKS.ESBUILD_PLUGINS | esbuild | hookContext | esbuild plugin object(s), same shape as the API's plugins option | | HOOKS.PREPROS_HTML | prepros | (html, { file, abs, exportPath, config }) | the modified HTML string — a waterfall hook (run with runWaterfall), so return the new string or null/undefined to leave it untouched | | HOOKS.PREPROS_PHP | prepros | ({ __root, config }) | absolute path(s) of .php file(s) to include_once in the prepros runtime once, before any page renders — for a plugin to PREPROS::registerTag() / registerHook() from PHP | | HOOKS.SCRIPTS_REGISTER | (none — engine-level) | ({ config }) | object(s) { name, file, trigger?, mount? } — a runnable PHP script, the plugin's counterpart of a project's own scripts/<name>.php + kirigami.yaml scripts: entry | | HOOKS.TASKS_REGISTER | (none — engine-level) | ({ config }) | object(s) shaped like a kirigami.yaml tasks: entry ({ name, type, ... }) — a build task, the plugin's counterpart of a project's own tasks: entry; its name must not collide with any other task | | HOOKS.COMMANDS_REGISTER | (none — engine-level) | ({ config }) | object(s) { name, description?, run } — same shape registerCommand() takes, an alternative to calling it directly |

SCRIPTS_REGISTER listeners each describe one script: name is what kiri run <name> (or Project#run(name)) invokes it by; file is an absolute path to the plugin's own .php file (resolve it the same way as the *_BEFORE/ *_AFTER example below); trigger — one of 'before-build', 'before-export', 'after-export' — fires it automatically at that checkpoint, same as a kirigami.yaml-declared script; mount is an optional array of glob patterns (relative to the project root) to mount into the sandbox first. A project's own scripts/<name>.php always wins over a plugin registering the same name. file must be inside the project or inside the package directory of an active plugin, so linked (npm link) and workspace plugins work too.

TASKS_REGISTER listeners each describe one build task — type can be a built-in or any registered task type, most often one the same plugin registers with registerTaskType() in the same register() call, so a project doesn't need its own tasks: entry to run it. Collected once, right after plugins load, and appended to the project's own tasks: list — so it goes through the exact same validation/build/export/watch path as any other task, and reload() re-collects it fresh (no accumulation across reloads).

COMMANDS_REGISTER listeners are collected right after plugins load and each routed through registerCommand() — so a duplicate name (against another hook entry or a directly-registered command) or a non-function run throws the same error either style produces. Prefer this over calling registerCommand() directly only for consistency with the other *_REGISTER hooks; functionally they're equivalent, since register() already has options/config in scope either way.

For *_BEFORE/*_AFTER, prefer an absolute path resolved from the plugin itself (as in the example above) — a relative path would be resolved from the cwd() of the project using kirigami, not from the plugin. esbuild:before / esbuild:after files are bundled as bare side-effect imports, so their order is preserved: before → entry → after.

*_BEFORE / *_AFTER fire for the first sass task and the first esbuild task only (first in tasks: order, plugin-injected tasks last), so a site with several stylesheets or bundles does not get each plugin's files repeated in all of them. SASS_FUNCTIONS and ESBUILD_PLUGINS still fire for every task, since each one needs them to compile. sass.before / sass.after from kirigami.yaml are not affected.

For SASS_FUNCTIONS, if the signature collides with one of kirigami's native functions (inline-file, img-asset, colors, font-*), the native one wins.

PREPROS_HTML fires once per rendered .html file. If no plugin registers a listener, kirigami-core doesn't even read the files back.


Register hooks inside the plugin’s default registration function so reload can register them again after resetting the shared registry. Registries are process-wide.

API

reset(hookName?)

Remove all listeners for one hook, or every hook when omitted. This does not clear commands (resetCommands() does that) or persistent caches. Resetting a registry does not cancel a dispatch that has already started. Do not mutate registrations during dispatch: listeners are iterated from a live Set. The core owns this during reload; plugins should normally retain and call their own unsubscribe functions.

Command registry

registerCommand(name, { description, run }) registers a plugin command. run(args, project) receives raw arguments and the loaded project. A duplicate name or non-function run throws. getCommand(name) returns the command or null; listCommands() returns all entries; resetCommands(name?) removes one or all. Register commands inside the default plugin function, with kirigami.type: "command" in its manifest — or return them from the HOOKS.COMMANDS_REGISTER hook instead (see Available hooks); kirigami-core routes hook entries through this same registerCommand(), so both styles share the same validation. Entries have { name, description, run }; descriptions default to an empty string, and listing preserves registration order. Returned entries are the stored mutable objects. The registry does not execute commands or validate their arguments.

Task-type registry

registerTaskType(name, definition) lets a normal plugin provide a type used by entries in kirigami.yaml's tasks: list. Register it inside the plugin's default registration function; project reload clears and rebuilds the task registry together with hooks and commands.

import { registerTaskType } from '@kirigami/sdk';

export default function register() {
	registerTaskType('manifest', {
		taskname: 'Generate manifest',
		canbuild: true,
		canwatch: false,
		validate(root, task) {
			if (!task.output) throw new Error('manifest tasks require output');
		},
		async run(root, task, exportPath) {
			// Generate task.output and return the standard task result shape.
			return { success: true, files: [task.output] };
		},
	});
}

The definition requires run(root, task, exportPath?). taskname defaults to the registered name; canbuild and canwatch default to false. validate(root, task) is optional and may be asynchronous. A watchable type must set canwatch: true and provide getWatcher(root, task), returning the same rule shape used by Kirigami's watch engine. Built-in task names take precedence and cannot be overridden.

getTaskType(name) returns one definition or null; listTaskTypes() keeps registration order; resetTaskTypes(name?) removes one or all definitions. Duplicate names and definitions without a callable run are rejected.

on(hookName, fn)

Registers a listener. Returns a function to unregister it (equivalent to calling off(hookName, fn)). The same function object is registered only once per hook; separate closures are separate listeners.

off(hookName, fn)

Unregisters a listener previously added with on().

run(hookName, ...args)

Runs every listener registered for a hook, in registration order, and flattens their results one level into a single array. Top-level null/undefined results are skipped; nested arrays and nulls inside returned arrays are preserved. A thrown error or rejected promise stops dispatch and rejects run(); the same failure rule applies to runWaterfall(). Used internally by kirigami-core — a plugin normally doesn't need to call run() itself.

runWaterfall(hookName, value, ...args)

Pipes value through every listener in registration order: each receives (value, ...args) and, unless it returns null/undefined, its return value becomes the input for the next. The shape kirigami-core uses for hooks that transform a single artefact (prepros:html) rather than collect contributions.

has(hookName)

true when at least one listener is registered for a hook — lets a task skip setup work when nothing hooks in.

HOOKS

Frozen object listing the known hook names (see table above). Prefer these over hand-typed strings.


Cache

A persistent key/value cache on disk, backed by node:sqlite (built into Node — no native compilation, unlike better-sqlite3). This is what kirigami-core uses for its own cache (representative colors, font metadata), but a plugin can just as well spin up its own:

import { Cache } from '@kirigami/sdk';
import path from 'node:path';

// A file dedicated to the plugin instead of sharing the core's.
const cache = new Cache(path.join(process.cwd(), '.my-plugin.db'));

cache.set('meta_home', { hello: 'world' }, 3600); // 1h TTL, in seconds (0 = never expires)
cache.get('meta_home'); // { hello: 'world' }, or null if missing/expired
cache.del('meta_home');
cache.purge();          // deletes all expired entries
cache.purge('meta_*');  // deletes every "meta_" entry, expired or not
cache.close();          // closes the underlying SQLite connection

new Cache() with no argument uses .node.db in the current working directory — the same file kirigami-core uses. Pass an explicit path to keep a plugin's cache separate.

| Method | Returns | Description | |---|---|---| | get(key) | value | null | Stored value, or null if missing/expired. | | set(key, val, ttl = 0) | boolean | Stores val (JSON-serialized); ttl in seconds, 0 = never expires. | | del(key) | boolean | Deletes one entry. | | purge(mask?) | number | No argument: deletes expired entries. With a glob mask ('meta_*'): deletes every matching entry, expired or not. Returns rows deleted. | | close() | void | Closes the SQLite connection (reopened lazily on the next call). |

Key naming. Every key must start with a namespace — one or more [a-z] characters followed by _ (colors_, font_, meta_, …). A key that doesn't match throws. That namespace is what purge(mask) targets: pass a glob like 'meta_*' to drop a whole namespace at once (* is the only wildcard). purge() with no argument keeps its original meaning — sweep expired entries only. Both forms return the number of rows deleted.

Values round-trip through JSON.stringify/JSON.parse: use JSON-compatible data. Dates, class instances, and Buffers lose their original type; circular values and BigInt throw. Stored null, missing entries, expired entries, and unparseable stored JSON all read as null.

Construction performs no I/O; the first operation opens the database. Create its parent directory yourself. The table is initialized only for a new file, so do not point it at an existing empty file or unrelated database. Calls are synchronous; SQLite errors propagate. del() returns whether a row existed. TTL uses whole seconds: an entry remains readable at its expiry second and expires after it. Reads do not delete expired rows; purge() does. Use 0 or a positive TTL; negative values are not validated and are not swept by purge().


TypeScript declarations

The shipped index.d.ts covers hooks, reset, Cache, and the command registry. Command<TProject, TResult> describes the host project and command result without importing the core package into the SDK. They default to unknown; provide your host type when registering or retrieving a typed command. Registration and lookup do not perform runtime type validation of the host.


Requirements

  • Node.js >= 24.0.0
  • npm >= 10.2.3
  • ESM only ("type": "module")

License

GPL-3.0-or-later © Maxime Larrivée-Roy, 2026