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

@laikacms/vite-plugin

v5.0.0

Published

Vite plugin for Laika CMS: mounts the storage, documents, and assets JSON:API in the dev/preview server, backed by a repository of your choice (a filesystem repository by default).

Downloads

580

Readme

@laikacms/vite-plugin

A Vite / Rolldown plugin that loads Laika CMS content as ES modules at build time.

Import content through the laika: protocol and each item is read from the documents or storage repository and emitted as a module with one named export per field. Because content is inlined at build time, this is the default and primary mode: a fully static, client-only build — no server, no JSON:API, nothing to deploy alongside your app. There's also an opt-in dev-server-only "local mode" that mounts a real JSON:API for tools that need one — see Local mode.

Install

npm install -D @laikacms/vite-plugin laikacms

Requires Vite >=8 (Rolldown-based). Works with rolldown directly too.

Usage

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

export default defineConfig({
  plugins: [laikacms({ dir: 'content' })],
});
// app code — read a single document
import { $key, body, title } from 'laika:doc/posts/hello';

// or a storage object
import site from 'laika:store/config/site';

Each content item becomes a module:

// laika:doc/posts/hello
export const title = 'Hello world';
export const date = '2026-01-02';
export const body = '# Hello…';
export const $key = 'posts/hello'; // repository metadata is $-prefixed
export const $language = 'en';
export default { title, date, body };

Importing { title } drops body from the bundle — the exports are independent bindings, so Rollup/ Rolldown tree-shakes whatever you don't read.

The laika: protocol

laika:<namespace>/<key> — the namespace picks the repository:

| Namespace | Repository | Read via | | --------- | ----------------------- | ------------------ | | doc | documents (ContentBase) | getDocument(key) | | store | storage | getObject(key) |

Local mode

The laika: protocol above is build-time-inline and stays the default. Separately, and off by default, the plugin can mount LaikaCMS's own JSON:API over its repositories while vite dev is running — for a JSON:API client (e.g. the Decap admin) that needs to read and write content against a real HTTP API in dev instead of a remote backend:

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

export default defineConfig({
  plugins: [laikacms({ dir: 'content', localApi: true })],
});

localApi: true mounts, under the default base path /__laika:

  • /__laika/storage — the storage repository's JSON:API (storage-api's buildJsonApi)
  • /__laika/documents — the documents repository's JSON:API (documents-api's buildJsonApi)
  • /__laika/assets — the assets repository's JSON:API (assets-api's buildAssetsApi), so Decap media uploads in dev land locally
  • /__laika/session — a trivial stub-identity responder; local mode does no real auth, so every request gets the same fixed identity. It exists because LaikaBackend.authenticate() (@laikacms/server) always pings ${apiUrl}/session to resolve a display identity, even when the local backend authenticates with a dummy token.

All three repository-backed sub-APIs serve the same repositories the laika: loader reads: by default a filesystem storage repository built from dir, with the documents and assets repositories derived from it via ContentBase. To serve different ones, pass them through the plugin's repositories option.

Pass an options object instead of true to override the base path:

laikacms({
  dir: 'content',
  localApi: {
    // Base path the storage/documents/assets sub-routes mount under.
    // Default: '/__laika'.
    basePath: '/__laika',
  },
});

Local mode is unauthenticated by design and only ever wired up from Vite's configureServer hook — nothing in the build phase or configurePreviewServer calls it, so a production build has no route to it by construction. If the dev server isn't bound to loopback (e.g. --host), the plugin logs a warning that the mounted API is reachable, unauthenticated, from the network.

mountLocalApi (and its LaikaLocalApiOptions type) are also exported directly from @laikacms/vite-plugin for callers who want to mount the same API onto their own dev server instance rather than going through the localApi plugin option.

import.meta.glob

Glob patterns over the protocol are expanded at build time by listing the repository — Vite's native glob only understands filesystem paths, so this plugin rewrites laika: globs itself:

// A blog index that bundles ONLY each post's title
const titles = import.meta.glob('laika:doc/posts/*', { import: 'title', eager: true });
// → { 'laika:doc/posts/a': 'Title A', 'laika:doc/posts/b': 'Title B', … }

// Lazy variant — dynamic import per entry
const posts = import.meta.glob('laika:doc/posts/*');
// → { 'laika:doc/posts/a': () => import('laika:doc/posts/a'), … }

eager, import: '<field>', and !-prefixed exclusion patterns are supported. * matches within a path segment; ** matches across segments.

TypeScript

The plugin generates types for every laika: import — and it does so by handing the real content data to the TypeScript compiler and letting it infer the types (no hand-written type text, so the types can never drift from what you import). Output goes to .laika/ (git-ignored automatically), referenced from a one-line laika-env.d.ts that is scaffolded at your project root:

// laika-env.d.ts  (commit this)
/// <reference path="./.laika/types.d.ts" />

Make sure your tsconfig.json includes it (root *.d.ts are included by default). Then import { title } from 'laika:doc/posts/hello' types title as whatever the compiler infers from the actual file. For import.meta.glob, a per-collection union type is generated so you can annotate the value type:

const posts = import.meta.glob<Posts>('laika:doc/posts/*', { eager: true });

MDX bodies

A markdown-serialized item (.md, .mdx, .markdown) deserializes to its frontmatter fields plus body, the prose. With mdx: true that prose is also written out as a real .mdx chunk under .laika/bodies/, and the module re-exports the compiled component as Body:

laikacms({ dir: 'content', mdx: true });
---
badge: Coming soon
heading: A hosted gateway
---

Self-install `laika-gateway`, a multi-tenant Worker.
import { badge, Body, heading } from 'laika:store/platform';

<Body components={{ code: props => <span className="font-mono" {...props} /> }} />;

This plugin never compiles MDX and does not depend on it. It emits the chunk; pair it with a plugin that compiles one — @mdx-js/rollup, ahead of your JSX plugin:

import mdx from '@mdx-js/rollup';

plugins: [
  laikacms({ dir: 'content', mdx: true }),
  { enforce: 'pre', ...mdx() },
  react({ include: /\.(jsx|js|mdx|tsx|ts)$/ }),
];

Notes:

  • The raw body string stays exported, and Body is kept out of the default export — importing the data object never drags the compiled component (or the MDX runtime) in with it.
  • Chunks are .mdx, so bodies are MDX rather than plain CommonMark: a literal < or { in prose has to be escaped.
  • Typing Body needs @types/mdx in your project — that is where mdx/types comes from.
  • A content field literally named Body is an error: it would shadow this export.

Hot reload

In vite dev, editing content on disk invalidates the matching laika: modules and reloads the page — including the .mdx chunk behind Body, which is rewritten before the reload is sent. This is powered by a repository change channel (StorageRepository.subscribeChanges); the filesystem repository implements it with a native recursive watch. Repositories without a push channel simply don't hot-reload. Types regenerate off the same channel.

Options

laikacms({
  // Directory the default filesystem repository reads, relative to the Vite
  // project root. Default: 'content'.
  dir: 'content',

  // File extension for keys without one. Default: 'json'.
  // Recognised out of the box: .json, .yaml/.yml, .md/.mdx/.markdown, .raw.
  defaultExtension: 'json',

  // Emit each item's `body` as an .mdx chunk and export it as `Body`.
  // Default: false. Requires an MDX plugin in the chain — see "MDX bodies".
  mdx: false,

  // TypeScript declaration generation. Default: true.
  // Pass { literals: true } to keep literal types (`'draft'`) instead of widening to `string`.
  typegen: true,

  // Dev-server hot reload on content change. Default: true.
  // Pass { coarse: true } to invalidate every laika: module on any change.
  hmr: true,
});

Bring your own repository

The filesystem repository is only the default. Pass any StorageRepository (the documents repository is derived from it via ContentBase):

import type { StorageRepository } from 'laikacms/storage';

const storage: StorageRepository = /* r2, s3, webdav, drizzle, … */;
laikacms({ storage });

…or supply both repositories yourself:

import { createRepositories } from '@laikacms/vite-plugin';

laikacms({ repositories: { storage, documents } });

License

MIT © Sem Postma