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

@jini-ai/cms

v0.7.2

Published

Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.

Readme

@jini-ai/cms

Content-model capability for Jini-hosted products.

The source tree includes content domains and Node adapters. CMS depends on shared kernel contracts and has no user-management dependency. Generic tool wiring is exported by @jini-ai/core; hosts provide CMS authorization and active-principal lookup ports. See API integration for current signatures and migration examples, and integration-report.md for pending protected work and deferred verification.

Forms email sender

@jini-ai/cms/forms/email-sender: createFormsEmailSender({ deps }), FormsEmailSenderDeps, FormEmailAction, FormEmailActionInput. Install the optional peer @jini-ai/platform@^0.6.15 when using this sender; that release includes @jini-ai/platform/mail/senders. The @jini-ai/cms/forms entry loads without platform installed. Tokens use stable field ids, never labels; built-in collisions use field.<id>. Built-ins: site_name, canonical site_url, and ISO submitted_at; templates/hosts own formatting. Files, hidden fields, IPs, credentials, undeclared keys, defaults and owner recipients are excluded. Only Email fields may supply recipients; choices export option values and booleans use "true"/"false". Configuration writes only through the injected guarded settings writer; no delivery effects occur here.

Layers

| subpath | runtime | contents | |---|---|---| | . / ./core | universal | Content contracts and types, ports, pure domain services, kernel registries. No Express, no node:*, no DOM. | | ./media | node | Media services, transform ports and preview policies. | | ./media/node | node | Filesystem blob stores, sharp image transforms and ffmpeg adapters. | | ./settings | universal | One-release compatibility re-export of @jini-ai/core/settings. | | ./settings/express | node | One-release compatibility re-export of @jini-ai/core/settings/express. | | ./widgets / ./widgets/markers / ./widgets/resolvers | universal | Widget contracts, markers and resolution services; no HTML parser. | | ./widgets/html | node | HTML scanning/substitution, fragment validation and plugin-export template resolution. |

Hosts that sanitize SVG uploads or ingest documents through ./media must install the optional peers dompurify and jsdom. They load lazily: without them, other media still works, but SVG and Office uploads fail. The optional jsdom peer accepts ^25.0.1 || ^26.0.0 || ^27.0.0; media uses the shared JSDOM window, DOMParser, XMLSerializer, basic DOM queries and close APIs.

Node-bound domain adapters depend on /core. The reverse is a boundary violation — if a core module wants something from a Node adapter, the dependency is backwards and the fix is a port (interface) in core, not a widened export.

Domain HTTP adapters live under their domain subpaths. Hosts can compose them using @jini-ai/http-kit and @jini-ai/server. Identity tool registration composition belongs to @jini-ai/user-management/server.

Generic settings now belong to @jini-ai/core/settings; the former CMS settings subpaths remain thin compatibility re-exports for one release.

@jini-ai/core/settings/express exports registerSettingsRoutes, the settings HTTP contracts and the CMS service/change-feed factories. Hosts provide route paths, permissions, workspace, readiness, principal resolution, authorization, scheduler, service and feed ports. Registration returns a disposer that closes active feeds. CMS adapters pass named request objects to settings dispatch and change-feed services; queryable index-name helpers return promises that callers must await. Install the optional peer express when using ./settings/express. The HTTP toolkit is a host composition choice and is not a CMS dependency. Readiness or principal-resolution failures return 401 UNAUTHENTICATED. Stream diagnostics are best-effort: a throwing reporter cannot prevent retries, cancellation of other schedules or ending the response.

Plugin embeds resolve only through the node entry: hosts pass their site-scoped admission port as ResolveHtmlPageEmbedsDeps.pluginExports to resolveHtmlPageEmbeds from @jini-ai/cms/widgets/html, which fills templates with parse5. The universal registry never resolves plugins. A missing port or a refused export keeps the authored fallback HTML. See the host adoption note.

Why the split exists before there is any code to split

The source of this port is an existing CMS runtime, and the boundary is drawn where a measured defect was.

There, the content model and its HTTP composition root share one src/ tree with nothing enforcing a boundary between them. Measured 2026-08-02:

  • 53 import edges point into the composition root. 22 of them are domain modules importing a 454-line RouteDeps type whose first line is import type { Express } from "express".
  • Git history shows the cost: no content module could be changed without also changing the server. features/theme and server co-changed in 71% of commits touching the former; identity and server in 45%.
  • 33 of 38 modules formed a single strongly-connected component — none could be extracted without dragging the other 32.

The decomposition itself was sound (propagation cost 11%, a textbook Martin instability gradient, domain modules that essentially never co-change with each other). What was missing was enforcement. Full analysis: ADS-memory/reports/refactors/2026-08-02-module-graph-analysis.md in the originating repo.

A package's exports map is precisely the enforcement a single src/ tree cannot provide. A consumer of @jini-ai/cms/core physically cannot reach a Node adapter, and /core physically cannot import a transport type, because the module resolver refuses. Porting into this shape from the first commit means that failure mode cannot recur here.

Port inventory

The kernel went first — the source repo's src/core/{ports,commands,events,tools} — because every domain module depends on it. Domain modules followed in no particular order: the source repo's git history shows they essentially never co-change with each other, so each ports independently without coordination.

Landed (directories under src/, each with its own subpath export): core, content-types, entries, media, navigation, presentation, settings, taxonomy, workspace, and trash. settings is now a compatibility shim, with implementation in @jini-ai/core/settings. ./core/tools re-exports the shared core permission helpers; ./media/import exports bounded image fetching with injected policies and ports.

./navigation stays universal: submitted menu HTML requires an injected normalizeMenuHtml dependency (normalizeHtml for mergeMenuHtml). Item-only writes and retained markup need no parser. Node hosts use ./navigation/node, which re-exports the navigation API and supplies parse5 normalization to create/update, merge and tool registration. The existing validation, raw-HTML permission checks, OCC and outbox owners remain unchanged; an unconfigured universal HTML write fails before saving rather than persisting markup that could escape its theme container.

Still to move: post, seo, comments, redirects, newsletter, members. Widgets are exposed through the universal and node entries above. The nested forms package is maintained by its separate owner and is excluded from this parent package.

Explicitly not ported:

  • src/server/** — Express composition root; superseded by @jini-ai/http-kit / @jini-ai/server.
  • Admin UI — @jini-ai/admin already owns that surface.

When porting tests, git mv them rather than re-authoring: re-authoring costs a full pass and still loses coverage.

Scripts

pnpm --filter @jini-ai/cms build
pnpm --filter @jini-ai/cms typecheck
pnpm --filter @jini-ai/cms test

Open cleanup

  • Add coverage thresholds. vitest.config.ts still has none. That was a deliberate omission while the package was a placeholder; it no longer is, so the reason has expired. Set them against what the ported modules actually measure — siblings run 98–100%.

Design decisions

The ./trash surface owns snapshot-only listing, keyset cursors, atomic marker/index writes, transactional follow-ups and the retention sweeper. Hosts inject their table adapters, reentrant transaction runner, IdGenerator, entity policy, Clock and scheduler. Native timer unref and error logging belong to the host. Actor/prior-marker/list filters are passed in the optional second object. Purge callers own human confirmation and must authorize each resolved index row; unavailable domains remain adapter-unavailable after authorization even when their entity policy denies them.