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

@napplet/vite-plugin

v0.10.0

Published

Vite plugin for napplet development — injects aggregate hash meta tag during dev and optionally generates NIP-5A manifest at build time for local testing

Readme

@napplet/vite-plugin

Vite plugin for napplet local development -- injects aggregate hash meta tags and optionally generates NIP-5A manifests for testing.

This is a development tool. For production deployment of napplets to nsites, use community deploy tools like nsyte which handle NIP-5A event creation and relay publishing.

Getting Started

What This Plugin Does

During dev mode, the plugin injects empty meta tags into your HTML so the napplet shim can find them:

<meta name="napplet-aggregate-hash" content="">
<meta name="napplet-napp-type" content="my-napp">

At build time (with VITE_DEV_PRIVKEY_HEX set), the plugin:

  1. Optionally rewrites local JS/CSS build assets into index.html when artifactMode: 'single-file' is enabled
  2. Walks the final dist/ artifact set and computes SHA-256 of each file
  3. Computes the aggregate hash per the NIP-5A algorithm (over the path tags alone)
  4. Creates a NIP-5D kind 35129 named-napplet manifest event — NIP-5A tag schema: one ['path', '/abs/path', '<sha256>'] per file plus one aggregate ['x', '<aggregateHash>', 'aggregate'] tag — and signs it
  5. Writes .nip5a-manifest.json to dist/
  6. Updates the meta tag in dist/index.html with the computed hash
  7. Injects <meta name="napplet-config-schema"> into dist/index.html if a configSchema is declared or discovered
  8. Embeds the schema as a ['config', ...] tag on the manifest (NOT folded into aggregateHash — the aggregate is path tags only, per NIP-5D §Identity)

The build-time manifest is for verifying the hash computation workflow locally, not for deploying to relays.

When to Use This

  • You are building a napplet and testing locally with a shell implementation
  • You want to verify aggregate hash computation before deploying

When NOT to Use This

  • Deploying napplets to production (use nsyte or similar)
  • Creating NIP-5A events for relay publishing (use dedicated deploy tools)
  • Runtime dependencies -- this plugin runs at build/dev time only

Installation

npm install -D @napplet/vite-plugin

Note: This is a devDependency. It is not needed at runtime.

Quick Start

// vite.config.ts
import { defineConfig } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';

export default defineConfig({
  plugins: [
    nip5aManifest({ nappletType: 'my-napp' }),
  ],
});

Configuration

Plugin Options

nappletType (required)

Type: string

The napp type identifier (e.g., 'feed', 'chat', 'profile'). This value is:

  • Injected as the content of the <meta name="napplet-napp-type"> tag
  • Used as the d tag in the kind 35129 manifest event

requires (optional)

Type: string[]

An array of service names this napplet requires from its host shell (e.g., ['audio', 'notifications']). When set:

  • Injects a <meta name="napplet-requires"> tag into HTML (comma-separated service names)
  • Adds ['requires', 'service-name'] tags to the kind 35129 manifest event

If the shell does not support all required capabilities, the napplet can detect this at runtime via window.napplet.shell.supports() or the shell can show a compatibility warning.

configSchema (optional)

Type: NappletConfigSchema | string | undefined

Declares a JSON Schema (draft-07+) describing the napplet's per-napplet configuration surface (NAP-CONFIG). At build time, the plugin:

  • Validates the schema against the NAP-CONFIG Core Subset (see Build-Time Guards below)

  • Embeds the schema as a ['config', JSON.stringify(schema)] tag on the kind 35129 manifest event

  • Injects <meta name="napplet-config-schema" content="{json}"> into dist/index.html so the napplet's shim can read it synchronously at install time

    The schema is not folded into aggregateHash: per NIP-5D §Identity the aggregate is the NIP-5A hash of the path tags alone, so a runtime can recompute and verify it. The config tag still carries the schema for a shell to act on.

Accepted forms:

| Value | Behaviour | |-------|-----------| | NappletConfigSchema object | Used directly | | string (path) | Resolved relative to the Vite project root; read + parsed as JSON | | undefined (omitted) | Falls through to convention-file discovery |

Discovery precedence (when configSchema is not provided):

  1. options.configSchema (inline object or path string) -- highest priority
  2. config.schema.json at the project root -- convention file
  3. napplet.config.ts / napplet.config.js / napplet.config.mjs at the project root, exporting a configSchema named export (or on the default export) -- dynamic import fallback

If none of the three paths resolve a schema, manifest/meta emission for the config tag is skipped silently -- build produces bytes identical to a pre-phase-114 napplet.

archetypes (optional)

Type: Array<string | { slug: string; naps?: string[]; contracts?: { protocol: string; eventKinds?: number[] }[] }>

Declares the NAAT archetype roles this napplet fulfills (napplet/naps ARCHETYPES.md). Each accepted protocol emits one ['archetype', slug, protocol, ...constraints] tag on the kind 35129 manifest event, where slug is the role slug, protocol is a single NAP-N wire format, and constraints such as kind:<number> are scoped to that protocol. A napplet may declare several archetype roles; a napplet with no archetype tag is fully valid.

nip5aManifest({
  nappletType: 'my-feed',
  archetypes: [
    { slug: 'feed', naps: ['NAP-5', 'NAP-6'] },
    { slug: 'note', contracts: [{ protocol: 'NAP-4', eventKinds: [1, 30023] }] },
  ],
});
// → emits ['archetype', 'feed', 'NAP-5']
// → emits ['archetype', 'feed', 'NAP-6']
// → emits ['archetype', 'note', 'NAP-4', 'kind:1', 'kind:30023']

Like the config tag, archetype tags are not folded into aggregateHash: per NIP-5D §Identity the aggregate is the NIP-5A hash of the path tags alone, so declaring archetypes never changes the napplet's content address. Blank slugs are skipped.

artifactMode (optional, v1.11+)

Type: 'external-assets' | 'single-file' Default: 'external-assets'

Controls the build artifact shape the plugin validates and hashes.

| Value | Behaviour | |-------|-----------| | 'external-assets' | Preserve Vite's default index.html + JS/CSS asset graph. Inline executable scripts are rejected. | | 'single-file' | Force Vite toward a single emitted artifact, inline local JS/CSS build asset references into index.html, and fail if local external assets remain before aggregate-hash and manifest generation. |

Use single-file when the napplet is meant to be served as a production-equivalent NIP-5A gateway artifact: a gateway-portable index.html loaded in an opaque-origin NIP-5D iframe without relying on separate local JS/CSS bundle routes.

// vite.config.ts
import { defineConfig } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';

export default defineConfig({
  plugins: [
    nip5aManifest({
      nappletType: 'my-napp',
      artifactMode: 'single-file',
    }),
  ],
});

In single-file mode:

  • The plugin first rejects any inline executable scripts already present in the built HTML.
  • It asks Vite/Rollup for a single-entry artifact shape (inlineDynamicImports, no CSS code-split, inline static assets) so ordinary static and dynamic imports are bundled before the close-bundle rewrite.
  • It then rewrites local stylesheet links and local script src tags to inline <style> / <script> blocks and removes those inlined JS/CSS files from dist/.
  • It fails the build if any local stylesheet, modulepreload, script src, or extra emitted file remains after rewriting.
  • The resulting index.html artifact bytes are used for the real ['path', '/index.html', <sha256>] manifest tag and aggregateHash input.
  • The aggregate hash is computed after inlining and before the self-referential aggregate-hash meta stamp is replaced.
  • config is emitted as its own manifest tag but does NOT participate in aggregateHash — the aggregate is the NIP-5A hash of the path tags alone.

Example (inline):

// vite.config.ts
import { defineConfig } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';

export default defineConfig({
  plugins: [
    nip5aManifest({
      nappletType: 'my-napp',
      configSchema: {
        type: 'object',
        properties: {
          theme: { type: 'string', enum: ['light', 'dark'], default: 'dark' },
          pollIntervalSeconds: { type: 'integer', minimum: 10, maximum: 3600, default: 60 },
        },
        required: ['theme'],
      },
    }),
  ],
});

Example (convention file):

// config.schema.json (at project root)
{
  "type": "object",
  "properties": {
    "theme": { "type": "string", "enum": ["light", "dark"], "default": "dark" }
  },
  "required": ["theme"]
}
// vite.config.ts -- no configSchema option; picked up from config.schema.json
nip5aManifest({ nappletType: 'my-napp' });

Example (napplet.config.ts fallback):

// napplet.config.ts (at project root)
import type { NappletConfigSchema } from '@napplet/nap/config/types';

export const configSchema: NappletConfigSchema = {
  type: 'object',
  properties: {
    theme: { type: 'string', enum: ['light', 'dark'], default: 'dark' },
  },
  required: ['theme'],
};

Build-Time Guards

The plugin validates the resolved schema against the NAP-CONFIG Core Subset at configResolved and throws a multi-line error (aborting the Vite build) on any of these rule violations:

| Error code | Trigger | |------------|---------| | invalid-schema | Root is not { type: "object", ... } | | pattern-not-allowed | Schema uses pattern anywhere in the tree (ReDoS risk per CVE-2025-69873) | | ref-not-allowed | Schema uses $ref in any form | | secret-with-default | A property marked x-napplet-secret: true also declares a default |

The walk recurses into properties, items, additionalProperties, patternProperties, oneOf, anyOf, allOf, not, definitions, and $defs -- the guard is wide even though the Core Subset is narrow.

Environment Variables

VITE_DEV_PRIVKEY_HEX

Type: string (hex-encoded 32-byte private key)

If set, the plugin signs the manifest event at build time. If not set, manifest generation is gracefully skipped (dev mode works without it).

Security: NEVER use a real private key here. Use a dedicated test key generated for local development only:

# Generate a test key (using nostr-tools or similar)
node -e "import('nostr-tools/pure').then(m => console.log(Buffer.from(m.generateSecretKey()).toString('hex')))"

Service Dependencies

Use the requires option when your napplet needs specific shell capabilities (like audio playback or push notifications) to function correctly.

// vite.config.ts
import { defineConfig } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';

export default defineConfig({
  plugins: [
    nip5aManifest({
      nappletType: 'my-music-app',
      requires: ['audio', 'notifications'],
    }),
  ],
});

What gets injected

With requires: ['audio', 'notifications'], the plugin injects into your HTML <head>:

<meta name="napplet-aggregate-hash" content="">
<meta name="napplet-napp-type" content="my-music-app">
<meta name="napplet-requires" content="audio,notifications">

At build time (with VITE_DEV_PRIVKEY_HEX set), the manifest event also includes requires tags:

{
  "kind": 35129,
  "tags": [
    ["d", "my-music-app"],
    ["path", "/index.html", "<sha256>"],
    ["x", "<aggregateHash>", "aggregate"],
    ["requires", "audio"],
    ["requires", "notifications"]
  ]
}

Runtime compatibility checking

The host shell reads <meta name="napplet-requires"> during napplet initialization and compares against its supported capabilities. Napplets can also check at runtime:

import '@napplet/shim';

if (!window.napplet.shell.supports('media')) {
  console.warn('Media NAP not available — some features disabled');
}

Build-Time Diagnostics

v0.29.0 adds a build-time safeguard enforced in closeBundle so misconfiguration fails loud before dist/ reaches a shell.

Inline scripts are supported (and expected)

Per NIP-5D a napplet is a single self-contained /index.html loaded via iframe.srcdoc with sandbox="allow-scripts" and no allow-same-origin — an opaque origin with no served URL. Its executable JS therefore lives inline; there is no origin from which the runtime could fetch an external <script src>. The plugin does not reject inline <script> elements. (An earlier version did, under an invented "shell-as-CSP-authority" model that NIP-5D does not define; that was removed — see napplet/web#53.)

When artifactMode: 'single-file' is set, the plugin additionally folds any local <script src>/<link rel="stylesheet"> build assets into index.html and deletes them, so the single file is the only served artifact. Pre-existing inline scripts in your built HTML are preserved verbatim.

How It Works

Dev Mode (transformIndexHtml)

Injects two meta tags into the HTML <head>:

<meta name="napplet-aggregate-hash" content="">
<meta name="napplet-napp-type" content="<nappletType>">

The empty aggregate hash tells the shell this is a development build. The shell reads these tags during napplet registration to resolve the aggregate hash for ACL scoping.

Build Mode (closeBundle)

Only runs if VITE_DEV_PRIVKEY_HEX is set:

  1. If artifactMode: 'single-file' is set, rewrites local JS/CSS references into index.html before hashing
  2. Walks dist/ directory recursively
  3. Computes SHA-256 hash of each file's contents
  4. Creates sorted hash lines: <sha256hex> <absolutePath>\n (NIP-5A: absolute paths, leading /)
  5. Computes aggregate hash (SHA-256 of sorted concatenation of the path-tag lines)
  6. Creates a kind 35129 manifest event with one ['path', '/abs/path', <sha256>] tag per file, one aggregate ['x', <aggregateHash>, 'aggregate'] tag, and requires tags if configured
  7. Signs with the test private key
  8. Writes .nip5a-manifest.json to dist/
  9. Updates the napplet-aggregate-hash meta tag in dist/index.html

API Reference

nip5aManifest(options)

Create a Vite plugin instance.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | options | Nip5aManifestOptions | Plugin configuration |

Returns: Plugin (Vite plugin)

Nip5aManifestOptions

interface Nip5aManifestOptions {
  /** Napplet type/dtag (e.g., 'feed', 'chat') */
  nappletType: string;

  /** Service dependencies this napplet requires (e.g., ['audio', 'notifications']). Optional. */
  requires?: string[];

  /**
   * Artifact output contract. Defaults to 'external-assets'. Set to
   * 'single-file' to inline local JS/CSS build assets into index.html before
   * NIP-5A aggregateHash and manifest generation.
   */
  artifactMode?: 'external-assets' | 'single-file';

  /**
   * JSON Schema (draft-07+) describing the napplet's config surface (NAP-CONFIG).
   * May be an inline object or a path string (resolved relative to the Vite
   * project root). Falls through to `config.schema.json` then `napplet.config.*`
   * discovery when omitted.
   */
  configSchema?: NappletConfigSchema | string;
}

Protocol Reference

License

MIT