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

@accup/vite-plugin-hogen

v0.1.0

Published

Vite plugin for emitting assets from TypeScript files matched by user-defined rules.

Readme

@accup/vite-plugin-hogen

Vite plugin for emitting assets from TypeScript files matched by user-defined rules.

For each matching file, the plugin evaluates the source with Vite's ModuleRunner, lets the rule emit assets and produce the final exports, and writes the result back as the file's compiled JS.

Installation

npm install --save-dev @accup/vite-plugin-hogen

Plugin setup

Register the plugin in vite.config.ts with the rules to apply.

import { hogen } from "@accup/vite-plugin-hogen/config";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    hogen({
      rules: [
        /* see below */
      ],
    }),
  ],
});

Plugin options

  • rules

    Rules registered with the plugin.

  • assetLeakPolicy

    Policy for the check that detects a Vite asset placeholder appearing in an emitted text asset's content. Values are "error", "warn", and "ignore". Defaults to "error".

Rule

A rule selects evaluation entries by id and decides how the matching file emits assets and produces exports.

import type { HogenRule } from "@accup/vite-plugin-hogen/config";

const rule: HogenRule = {
  name: "my-rule",
  testEntry: (id) => /\.my\.ts(\?.*)?$/u.test(id),

  // Adapter hook, emits during evaluation.
  adapterKey,
  createAdapter,

  // Evaluated-module hook, emits after evaluation.
  processModule,
};

Both hooks are optional. adapterKey and createAdapter are set together when the adapter hook is used.

Evaluation-only rule

A rule may declare no hook at all. The plugin then evaluates the source with ModuleRunner and serializes the resulting exports as static JS. A matching file becomes a build-time computation whose result is compiled into the output bundle without any asset emit.

import type { HogenRule } from "@accup/vite-plugin-hogen/config";

export function createConstRule(): HogenRule {
  return {
    name: "const",
    testEntry: (id) => /\.const\.ts(\?.*)?$/u.test(id),
  };
}

A matching file uses Node APIs at build time and produces plain values.

// version.const.ts
import { readFileSync } from "node:fs";

const pkg: { version: string } = JSON.parse(
  readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
);

export const version = pkg.version;

The compiled module is plain JS with the resolved value.

export const version = "1.2.3";

Adapter-based rule

Use the adapter hook when each emit happens during the file's evaluation. The file imports helpers that read the adapter and call adapter.emit per call site. Each helper returns the URL accessors of the asset it emitted.

Declare the adapter interface and a key.

import type { HogenAdapter } from "@accup/vite-plugin-hogen/config";
import type { HogenEmittedAsset } from "@accup/vite-plugin-hogen/config";
import { createAdapterKey } from "@accup/vite-plugin-hogen/config";

export interface SnapshotAdapter extends HogenAdapter {
  /** Emit a text snapshot and return its URL accessors */
  readonly snapshot: (text: string) => HogenEmittedAsset;
}

export const ADAPTER_KEY = createAdapterKey<SnapshotAdapter>("__snapshot_adapter__");

Build the rule.

import type { HogenRule } from "@accup/vite-plugin-hogen/config";

export function createSnapshotRule(): HogenRule<SnapshotAdapter> {
  return {
    name: "snapshot",
    testEntry: (id) => /\.snapshot\.ts(\?.*)?$/u.test(id),
    adapterKey: ADAPTER_KEY,
    createAdapter: (context) => ({
      snapshot(text) {
        return context.emit({
          type: "asset",
          name: "snapshot",
          source: text,
          mimeType: "text/plain",
        });
      },
    }),
  };
}

User code reaches the adapter through readAdapter.

import { readAdapter } from "@accup/vite-plugin-hogen";
import type { HogenEmittedAsset } from "@accup/vite-plugin-hogen";

import { ADAPTER_KEY } from "./adapter";

export function snapshot(text: string): HogenEmittedAsset {
  return readAdapter(ADAPTER_KEY).snapshot(text);
}

A matching file emits one asset per call. Each helper return value carries the URL accessors of the emitted asset, so each named export has the HogenEmittedAsset type at both the source and consumer side.

// notes.snapshot.ts
import { snapshot } from "./snapshot";

export const intro = snapshot("Hello, world.");
export const detail = snapshot("Longer payload.");

A consumer picks the accessor that matches the embedding context.

import { intro } from "./notes.snapshot";

const href = intro.absolutePath;

Evaluated-module-based rule

Use processModule when the rule reads the file's evaluated exports and decides what to emit. Helpers the source code imports brand each value; processModule detects the brand after evaluation and replaces every branded value with a URL.

Each named export keeps the same name across the source and the compiled output, so the source-side and consumer-side types stay aligned. The branding helper declares the return type that consumers receive, while its runtime value carries the data the rule needs.

Declare the value shape, the brand, and the helpers.

const REPORT_KEY = "__report__";

export interface Report {
  readonly title: string;
  readonly total: number;
}

/**
 * Brand a report value so the rule can find it among the evaluated exports.
 *
 * The runtime value carries the report data, while the declared return type matches the URL the rule writes into the compiled exports.
 *
 * @param report report data
 * @returns URL of the emitted report
 */
export function defineReport(report: Report): string {
  Object.defineProperty(report, REPORT_KEY, {
    value: true,
    enumerable: false,
    configurable: false,
    writable: false,
  });
  // The runtime value is replaced by a URL in the rule's processModule.
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
  return report as unknown as string;
}

export function isReport(value: unknown): value is Report {
  if (typeof value !== "object" || value == null) {
    return false;
  }
  return REPORT_KEY in value;
}

Build the rule.

import type { HogenRule } from "@accup/vite-plugin-hogen/config";

import { isReport } from "./report";

export function createReportRule(): HogenRule {
  return {
    name: "report",
    testEntry: (id) => /\.report\.ts(\?.*)?$/u.test(id),
    processModule(module) {
      const replaced = Object.entries(module.exports).map(([name, value]) => {
        if (!isReport(value)) {
          return [name, value] as const;
        }

        const url = module.emit({
          type: "asset",
          name,
          source: JSON.stringify(value),
          mimeType: "application/json",
        }).absolutePath;
        return [name, url] as const;
      });

      return Object.fromEntries(replaced);
    },
  };
}

A matching file calls the helper.

// sales.report.ts
import { defineReport } from "./report";

export const sales = defineReport({
  title: "Sales 2024",
  total: 12345,
});

Consumers see the declared return type of the helper, which matches the URL the rule writes into the compiled exports.

import { sales } from "./sales.report";
// sales has type `string` and is the URL of the emitted JSON at runtime

processModule may also return exports without calling module.emit. The rule then only transforms values; no asset reaches the output.

Mixed rule

A rule can declare both hooks. createAdapter exposes per-call helpers that emit during evaluation; processModule reads the resulting exports and emits or rewrites values that need post-evaluation handling. The two styles compose inside one file, and every export keeps the declared return type at the source and consumer side as long as each helper aligns its return type with the value the rule writes into the compiled exports.

import type { HogenRule } from "@accup/vite-plugin-hogen/config";

import { isReport } from "./report";

export function createCatalogRule(): HogenRule<CatalogAdapter> {
  return {
    name: "catalog",
    testEntry: (id) => /\.catalog\.ts(\?.*)?$/u.test(id),
    adapterKey: ADAPTER_KEY,
    createAdapter: (context) => ({
      attach(input) {
        return context.emit({
          type: "asset",
          name: input.name,
          source: input.source,
          mimeType: input.mimeType,
        });
      },
    }),
    processModule(module) {
      const replaced = Object.entries(module.exports).map(([name, value]) => {
        if (!isReport(value)) {
          return [name, value] as const;
        }

        const url = module.emit({
          type: "asset",
          name,
          source: JSON.stringify(value),
          mimeType: "application/json",
        }).absolutePath;
        return [name, url] as const;
      });

      return Object.fromEntries(replaced);
    },
  };
}

A matching file mixes both emit styles. attach emits during evaluation and returns the URL accessors directly; defineReport brands the value, and processModule rewrites it into a URL after evaluation.

// store.catalog.ts
import { attach } from "./attach";
import { defineReport } from "./report";

export const logo = attach({
  name: "logo",
  source: "<svg>...</svg>",
  mimeType: "image/svg+xml",
});

export const sales = defineReport({
  title: "Sales 2024",
  total: 12345,
});

Rule nesting

A file matched by one rule can import a file matched by another rule. The inner ModuleRunner that evaluates the entry hands a cross-rule id back to the main plugin so the main plugin processes it with the outer Rollup context, and the entry's evaluation receives the imported module's compiled exports.

// welcome.snap.ts
import { snapshot } from "./snapshot";

export const greeting = snapshot("Welcome.");
// page.bundle.ts
import { defineBundle } from "./bundle";
import * as welcome from "./welcome.snap.ts";

export const home = defineBundle({
  title: "Home",
  items: [{ label: "greeting", href: welcome.greeting.absolutePath }],
});

Module evaluation

The plugin evaluates each id at most once per session. After the first transform of a matched file, subsequent imports of the same id reuse the cached transform result, so emits and processModule run exactly once per session.

In dev mode, modifying a matched file invalidates its cached entry and every cached entry that reached it through a cross-rule import. Every id reached through the cross-rule delegate path is added to Vite's watcher, so a change to a file imported only by another evaluated file still triggers HMR.

Emit input

emit accepts two input shapes.

  • type: "file"

    Fixed-path asset. The asset is written under the given fileName. Set rule.testFile to the same path predicate so the dev server serves the file at that path and forces a full reload when the source module changes.

  • type: "asset"

    Dynamic-path asset. The output path is derived from name and a content hash.

source is a string or a Uint8Array. mimeType is read by the dev middleware to set the response Content-Type and is ignored at build time.

Emit result

emit returns a HogenEmittedAsset carrying three URL accessors.

  • absolutePath

    Absolute path including Vite's base prefix. Use this when embedding the URL inside another asset's content.

  • relativePath

    Path relative to the bundle output root.

  • assetRef

    Value safe to embed in a JS chunk. At build time this is a Vite asset placeholder that Vite resolves to the final URL during bundling.

Embedding assetRef inside another asset's content leaks the unresolved placeholder into the output. The plugin checks every text-typed type: "asset" emit for this pattern and reports a match according to the assetLeakPolicy option.