Skip to content

DocsGuidesPlugins

On this page

Plugins

A plugin extends an app at build time (Vite), at request time (middleware), through app hooks (observability/caching), and through the asset pipeline.

ts
// ramonyx.config.ts
import { imageOptimizer } from "@ramonyx/plugin-image";
import { defineConfig } from "ramonyx";
import { analytics } from "./src/plugins/analytics.ts";

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

ramonyx dev|build|start loads plugins from the config. A custom server entry (src/server.ts) still receives their hooks and is wrapped by their middleware — plugins are never duplicated.

Writing a plugin

ts
// src/plugins/analytics.ts
import { definePlugin } from "@ramonyx/bundler";
import type { Middleware } from "ramonyx";

const middleware: Middleware = async (context, next) => {
  const started = performance.now();
  const response = await next();
  console.log(context.request.method, new URL(context.request.url).pathname, response.status, Math.round(performance.now() - started));
  return response;
};

export const analytics = () =>
  definePlugin({
    name: "analytics",
    middleware: () => middleware,
    hooks: () => ({
      onLoaderEnd: (event) => {
        /* record loader timing */
      },
    }),
  });
Hook Runs Typical use
vite build/dev extra Vite plugins (CSS, fonts, aliases)
middleware before routing logging, headers, early blocking
hooks request lifecycle timing, metrics, invalidation
assets build, per file derived outputs (image variants) + dimensions

middleware must be edge-safe (no node:*) if you deploy the edge artifact. Middleware runs before application middleware, in plugin order.

Asset hook

ts
export const plugin = definePlugin({
  name: "thumbnails",
  async assets(_context, asset) {
    if (!asset.path.endsWith(".png")) return undefined;
    return {
      meta: { width: 800, height: 600 },
      outputs: [{ path: "img/hero-400.webp", data: encoded, width: 400, format: "webp" }],
    };
  },
});

Derived files are hashed automatically and recorded as variants in the manifest, so assets.image() and Image can render them.

Publishing a plugin

Third-party plugins live in node_modules, where Node does not strip TypeScript. Ship compiled JavaScript:

json
{
  "name": "ramonyx-plugin-analytics",
  "type": "module",
  "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
  "peerDependencies": { "@ramonyx/bundler": "^0.4.0", "ramonyx": "^0.4.0" },
  "engines": { "node": ">=24" }
}
  • Use peerDependencies (not dependencies) so types/contracts follow the app's Ramonyx version.
  • Heavy dependencies should be optional peers (peerDependenciesMeta) and imported lazily with a clear error — see how @ramonyx/plugin-image treats sharp.
  • Test the hooks directly and in a real app (ramonyx build|dev|start).
  • Contract stability: the plugin contract becomes stable after one release cycle without additive changes. Until then it may gain optional hooks (always additive).

Limitations (honest)

  • No plugin registry or ramonyx add — install with your package manager.
  • No per-route/per-loader plugin hooks (pre-routing middleware + app hooks only).
  • Plugin order is config order; there is no explicit priority field.
Edit this page on GitHub ↗