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

@indago/hyper-json

v1.2.0

Published

JSON Schema → strict validation + auto-generated TypeScript types + ambient module declarations for typed JSON content imports.

Readme

HyperJson


What is HyperJson?

HyperJson is the structured-data counterpart to HyperDown. Where HyperDown handles Markdown/MDX prose, HyperJson handles JSON content — playlists, photo albums, profiles, skills, project lists, and any other collection that is better described as data than as documents.

It does three things:

  1. Validates every JSON content file against the schema.json that sits next to it, at build time, using Ajv.
  2. Generates TypeScript types from each schema (via json-schema-to-typescript) so your content is typed end-to-end.
  3. Emits ambient module declarations so importing a content file — e.g. import data from "@content/music/en/playlists.json" — yields a fully-typed value with no manual casting.

A small set of client-side React hooks (filter / search / sort / paginate / compose) rounds it out for building content explorers and gallery UIs. HyperJson has no dependency on front-matter — all front-matter logic lives in HyperDown.

Architecture at a glance

flowchart LR
  subgraph Repo["Your repo"]
    S["schema.json"]
    J["*.json content<br/>(en/ · pt-BR/)"]
  end
  subgraph Build["Build time · hyperjsonValidationPlugin"]
    AJV["Ajv validation<br/>strict · failOnError"]
    GEN["json-schema-to-typescript<br/>codegen · worker pool"]
    S --> AJV
    J --> AJV
    S --> GEN
    GEN --> T["Generated types +<br/>ambient module declarations"]
  end
  subgraph App["Your app"]
    IMP["typed import<br/>@content/**/*.json"]
    HOOKS["headless hooks<br/>filter · search · sort · paginate"]
    T --> IMP --> HOOKS
  end
  AJV -->|"invalid ⇒ build exits non-zero"| FAIL["❌ build fails"]

Everything happens at build time: invalid content fails the build, valid content arrives fully typed at every import, and nothing runs at request time.


Feature highlights

  • Build-time validation of JSON content against per-folder schema.json.
  • 🧬 Generated types for every content type, named after each schema's title.
  • 📦 Ambient module declarations for typed @content/**/*.json imports.
  • Parallel codegen — a bounded worker pool (configurable via HYPERJSON_CONCURRENCY).
  • ⚙️ Vite plugin that validates + generates on build and serves virtual:hyperjson-config.
  • 🗺️ Sitemap plugin that turns JSON collections into sitemap.xml URLs, with an append mode that merges into a sitemap other tools also write to.
  • 🪝 Headless React hooks for filtering, searching, sorting, and paginating data.
  • 🧰 Full CLI (hyperjson) and an MCP server (hyperjson-mcp).

Installation

# bun (recommended)
bun add @indago/hyper-json

# npm / pnpm / yarn
npm install @indago/hyper-json

Peer dependencies

| Peer | Range | Notes | | ----------- | --------- | ----------------------- | | react | ^19.2.6 | required for the hooks | | react-dom | ^19.2.6 | required for the hooks | | vite | ^8.0.14 | required for the plugin |

node >= 20 is required for the CLI and codegen. Codegen uses the in-process json-schema-to-typescript API — no extra tooling needed at generation time.


Content layout

HyperJson scans a content directory (default src/content). Each content category is a folder containing a schema.json and one or more JSON data files, optionally grouped by locale (en/, pt-BR/):

src/content/
└── music/
    ├── schema.json          # JSON Schema describing the data shape
    ├── en/
    │   ├── playlists.json
    │   └── favorites.json
    └── pt-BR/
        └── playlists.json

The schema.json title becomes the generated TypeScript type name. Validation scans the en/ and pt-BR/ locale subdirectories as well as JSON files at the category root (excluding schema.json).


Quick start

1. Scaffold the config

bunx @indago/hyper-json init

Creates hyperjson.config.json:

{
  "$schema": "./node_modules/@indago/hyper-json/schemas/hyperjson.config.schema.json",
  "contentDir": "src/content",
  "validation": { "strict": true, "failOnError": true },
}

2. Add the Vite plugin

// vite.config.ts
import { hyperjsonValidationPlugin } from "@indago/hyper-json";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [hyperjsonValidationPlugin()],
});

3. Create a content type

bunx @indago/hyper-json create-content-type \
  --name music \
  --locales "en,pt-BR" \
  --fields "title:string:required;genre:string;url:string"

4. Validate & generate

bunx @indago/hyper-json validate
bunx @indago/hyper-json generate

5. Import content with full typing

import enPlaylists from "@content/music/en/playlists.json";
import type { MusicContentSchema } from "@indago/hyper-json";

// `enPlaylists` is typed via the generated ambient module declaration.

The @content/* import alias must be declared in your tsconfig.json compilerOptions.paths pointing at ./src/content/*. HyperJson reads that alias to know which prefix to declare the ambient modules under.


CLI reference

The hyperjson binary is installed with the package. Run it via bunx @indago/hyper-json <command>.

hyperjson <command> [target] [options]

| Command | Summary | | ------------------------------------------------------- | ------------------------------------------- | | init | Scaffold a default hyperjson.config.json. | | validate [target] | Validate config and/or JSON content. | | generate (alias gen) | Generate TypeScript types from schemas. | | create-content-type | Scaffold a new JSON content type. |

Scaffolds hyperjson.config.json in the current directory. Skips with a warning if the file already exists.

bunx @indago/hyper-json init

Validates the config and/or all JSON content files against their schemas. target is config, content, or both (default: both). Exits non-zero when any file fails.

The path flag overrides the default location for the chosen target: the config file for config, the content directory for content. It is ignored when target is both.

| Option | Default | Description | | ------------------- | -------------- | ----------------------------------------------------------------------------------- | | -p, --path <path> | target default | Path to the file/dir matching target (config or content). Ignored for both. |

bunx @indago/hyper-json validate
bunx @indago/hyper-json validate content
bunx @indago/hyper-json validate config --path ./apps/web/hyperjson.config.json

Generates TypeScript types for every content schema and writes the ambient module declarations. Runs the codegen with a parallel worker pool (see Codegen).

bunx @indago/hyper-json generate
bunx @indago/hyper-json gen            # alias
HYPERJSON_CONCURRENCY=2 bunx @indago/hyper-json generate

Scaffolds a new JSON content type: a schema.json and empty { "<wrapper>": [] } data files per locale. With neither --fields nor --fields-json, it launches an interactive builder that supports nested objects, arrays, and recursion.

| Option | Default | Description | | ---------------------- | --------------------- | ------------------------------------------------------------ | | --name <name> | — | Content folder name. | | --title <title> | <Name>ContentSchema | Schema title (becomes the generated type name). | | --locales <locales> | en | Comma-separated locale codes. | | --fields <fields> | — | Semicolon-separated flat name:type[:required] definitions. | | --fields-json <json> | — | JSON FieldSpec[] — nested objects, arrays, recursion. | | --content-dir <dir> | src/content | Content directory. | | --wrapper <prop> | items | Top-level array property name. |

Flat field types: string, number, integer, boolean, string[], enum, date (string with a YYYY-MM-DD pattern). A field is required when the third segment is the literal required.

bunx @indago/hyper-json create-content-type \
  --name skill \
  --locales en \
  --fields "name:string:required;level:string:required"

For nesting and recursion, pass a FieldSpec[] to --fields-json. Each spec is { name, type, required?, enumValues?, format?, fields?, items?, def?, ref? }object nests fields, array nests an items element spec, and a named def can be reused with ref (including by itself, for trees/menus):

bunx @indago/hyper-json create-content-type --name menu \
  --fields-json '[
    {"name":"label","type":"string","required":true},
    {"name":"children","type":"array","items":{"name":"","type":"ref","ref":"MenuItem"}}
  ]'

MCP server

hyperjson-mcp (declared in bin) is an MCP stdio server that wraps the same CLI as MCP tools for AI agents.

bunx --package @indago/hyper-json hyperjson-mcp

Tools: hyperjson_init, hyperjson_validate, hyperjson_generate, hyperjson_create_content_type. The creation tool requires name + fields — interactive prompts are disabled under MCP.


Vite plugin & virtual module

hyperjsonValidationPlugin(contentDir?)

On buildStart the plugin validates every JSON content file. If validation.failOnError is in effect and any file is invalid, the build exits with a non-zero code. Otherwise it runs codegen (types + ambient modules).

It also serves the virtual:hyperjson-config module — the parsed hyperjson.config.json as a default export, available in both dev and build:

import config from "virtual:hyperjson-config";
// → { contentDir: "src/content", validation: { strict, failOnError } }

contentDir (the plugin argument) overrides the config's contentDir when provided.

hyperjsonSitemapPlugin({ appRootDir? })

A build-only plugin (apply: "build") that turns JSON collections into sitemap.xml entries. On closeBundle it reads the optional sitemap block of hyperjson.config.json, walks each declared collection, and emits one <url> per item — locale-prefixed when an i18n block is present. If there is no sitemap block it no-ops (it never throws). Import it from @indago/hyper-json/plugins:

// vite.config.ts
import { hyperjsonValidationPlugin } from "@indago/hyper-json";
import { hyperjsonSitemapPlugin } from "@indago/hyper-json/plugins";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    hyperjsonValidationPlugin(),
    // …any other sitemap-writing plugin (router, @indago/hyper-down)…
    hyperjsonSitemapPlugin(), // append mode: must run LAST among sitemap writers
  ],
});

For each collections[] entry the plugin reads <contentDir>/<file> (per-locale when i18n.strategy === "folder"), drills into itemsPath when given, and maps every item to ${siteUrl}${localePrefix}${basePath}/${item[idField]}. lastmod comes from the JSON file's modification time (JSON has no front-matter date to read). staticRoutes are emitted verbatim, just like HyperDown's plugin.

mode mirrors @indago/hyper-down's sitemap plugin, but the default is "append" (not "override") — this plugin's whole purpose is to layer JSON-content URLs onto a sitemap the router and/or HyperDown already wrote:

  • "append" (default) — preserves any <url> already present at outputPath and overlays this run's URLs, deduplicating by <loc> (this run wins). Because it preserves whatever is already on disk, this plugin must run after every other tool that writes the same file in the build.
  • "override" — always writes a fresh sitemap from this config alone.

Dedup normalizes locs first (trailing slash, fragment, scheme/host case ignored), then keeps the last entry on a tie — both within this run's own staticRoutes + collection URLs, and against whatever was already on disk.

Ordering contract. In append mode hyperjsonSitemapPlugin reads the existing sitemap.xml from disk and merges into it, so it has to be the last sitemap writer in the pipeline. A typical site puts the router/static sitemap first, then hyperdownSitemapPlugin({ mode: "append" }) for MDX content, then hyperjsonSitemapPlugin() for JSON content — each layer preserving the previous one.


Programmatic API

The package exposes three entry points:

| Import | Provides | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | @indago/hyper-json | loadHyperJsonConfig, validateHyperJsonConfig, validateContentSchemas, hyperjsonValidationPlugin, loggers, and the generated content/config types. | | @indago/hyper-json/hooks | Client-side React hooks: useFilter, useSearch, useSort, usePaginate, useComposed. | | @indago/hyper-json/plugins | Vite plugins: hyperjsonValidationPlugin, hyperjsonSitemapPlugin. |

Validation & config

import {
  loadHyperJsonConfig, // (appRootDir) => HyperJsonConfiguration (validated)
  validateHyperJsonConfig, // (config, path?) => boolean (type guard)
  validateContentSchemas, // (contentDir?) => { passed, failed, results, schemaDirs }
} from "@indago/hyper-json";

Codegen

HyperJsonCodegen drives the type generation. Each content schema compiles through the in-process json-schema-to-typescript API, run through a bounded promise pool. It writes only into the consuming app's .hyper-json/ tree.

import { HyperJsonCodegen } from "@indago/hyper-json"; // via the package's codegen export

const codegen = new HyperJsonCodegen({
  appRootDir: process.cwd(),
  concurrency: 4, // optional
});

await codegen.generate();

The concurrency limit is resolved in priority order:

  1. the explicit concurrency option, then
  2. the HYPERJSON_CONCURRENCY environment variable, then
  3. cpus().length - 1 (minimum 1) — leaving a core free to keep the build responsive.

Ambient module declarations are written after all per-schema types settle, since they aggregate every content file.

Hooks

Headless, in-memory React hooks for shaping arrays of content. All are pure and useMemo-backed.

import {
  useFilter, // (data, FilterConfig[]) => T[]
  useSearch, // (data, query, fields) => T[]
  useSort, // (data, SortConfig | null) => T[]
  usePaginate, // (data, page, perPage) => { items, page, totalPages, total }
  useComposed, // filter → search → sort → paginate, all in one
} from "@indago/hyper-json/hooks";
const { paginated, filtered } = useComposed(playlists, {
  filters: [{ key: "genre", value: selectedGenre }], // "All"/undefined ⇒ skipped
  searchQuery,
  searchFields: ["title", "artist"],
  sort: { key: "title", dir: "asc" },
  page,
  perPage: 12,
});

// paginated.items, paginated.totalPages, paginated.total, …

Configuration reference

hyperjson.config.json

Validated against schemas/hyperjson.config.schema.json.

| Key | Type | Default | Description | | ------------------------ | --------- | ------------- | ------------------------------------------------------------------------ | | contentDir | string | src/content | Base directory for JSON content, relative to the app root. Required. | | validation.strict | boolean | true | Reject properties not declared in the schema. | | validation.failOnError | boolean | true | Exit non-zero on any validation error. | | sitemap | object | — | Optional. Configures hyperjsonSitemapPlugin (see below). | | i18n | object | — | Optional locale config; drives locale-prefixed sitemap URLs (see below). |

sitemap (optional — hyperjsonSitemapPlugin)

{
  "sitemap": {
    "siteUrl": "https://example.com", // no trailing slash
    "outputPath": "./public/sitemap.xml",
    "mode": "append", // "append" (default) | "override"
    "staticRoutes": [{ "path": "/music", "priority": "0.6", "changefreq": "monthly" }],
    "collections": [
      {
        "name": "music-favorites",
        "file": "music/favorites.json", // relative to contentDir (and the locale folder)
        "itemsPath": "items", // wrapper key holding the array (omit for a top-level array)
        "basePath": "/music", // URL prefix for each item
        "idField": "id", // item property used as the URL segment (default "id")
        "priority": "0.6",
        "changefreq": "monthly",
      },
    ],
  },
}

| Key | Type | Default | Description | | ------------------------- | ---------- | ------------ | ------------------------------------------------------------------------ | | siteUrl | string | — | Origin without a trailing slash. Required when sitemap is present. | | outputPath | string | — | Where to write the sitemap, relative to the app root. Required. | | mode | string | "append" | "append" merges into an existing file; "override" rewrites it. | | staticRoutes[] | object[] | [] | { path, priority, changefreq } — emitted verbatim under siteUrl. | | collections[] | object[] | [] | JSON collections expanded into one URL per item. | | collections[].file | string | — | JSON file relative to contentDir (and the locale folder under i18n). | | collections[].itemsPath | string | (whole file) | Dotted path drilled into before treating the value as an array. | | collections[].idField | string | "id" | Item property used as the final URL segment. | | collections[].basePath | string | — | URL prefix; item URL = ${siteUrl}${localePrefix}${basePath}/${id}. |

With an i18n block whose strategy is "folder", each collection file is resolved per-locale as <contentDir>/<dir>/<locale>/<name>.json and non-default locales are prefixed (/pt, …), matching HyperDown's behavior. lastmod is taken from each file's modification time. In append mode (the default), place this plugin after every other sitemap writer — it preserves whatever is already on disk and layers its URLs on top.

Auto-generated files — do not edit: the per-schema .hyper-json/src/content/*/types.ts and the aggregate .hyper-json/src/content/generated.d.ts are produced by codegen and will be overwritten — re-run bunx @indago/hyper-json generate after changing a schema.json. (Inside this repo, the package's own src/lib/types.ts is dev-time codegen: bun run gen:types, run automatically on prebuild.)


License

MIT © Zaú Júlio