NUMU Docs
Contact
APIThemes
Partner Apps
APIThemes
Partner Apps
  1. CLI & Vite plugin
  • 🗂️ Themes overview
  • 🚀 Start here
  • Glossary
  • Theme engine
    • Architecture
    • BYOT contract
    • Customizer
    • Federation runtime
    • Page data contract
    • Theme manifest
  • Theme SDK
    • Components
    • Federation helpers
    • Hooks
    • SDK overview
    • Type definitions
  • CLI & Vite plugin
    • CLI commands
    • CLI overview
    • Lint rules
    • Section library
    • Vite plugin
  • Storefront host
    • API proxies
    • Built-in fallbacks
    • BYOT fork
    • Routing
    • Storefront overview
  1. CLI & Vite plugin

Vite plugin

@numueg/theme-plugin — added to every theme's vite.config.ts.

Install + register#

Scaffolded by numu-theme init:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import numuTheme from "@numueg/theme-plugin";

export default defineConfig({
  plugins: [
    react(),
    numuTheme({
      // all options optional — sensible defaults
      themeRoot: ".",
      schemaDir: "schemas",
      localeDir: "locales",
      assetDir: "assets",
    }),
  ],
  build: {
    lib: {
      entry: "src/main.tsx",
      formats: ["es"],
      fileName: "theme",
    },
    cssCodeSplit: false,
    rollupOptions: {
      external: [
        "react",
        "react-dom",
        "react/jsx-runtime",
        "react-dom/client",
        "@numueg/theme-sdk",
      ],
      output: {
        assetFileNames: "[name][extname]",
        entryFileNames: "[name].js",
      },
    },
  },
});

What the plugin does#

1. Contract validation#

Hooks buildStart + closeBundle. On buildStart, parses src/main.tsx and asserts:
A named export called mount exists
mount is a function
React, ReactDOM, and @numueg/theme-sdk appear in rollupOptions.external
On closeBundle, scans the emitted theme.js and asserts no copy of React snuck in (greps for react.production.min's telltale strings).
If any assertion fails, the build aborts with a specific remediation message.

2. Schema codegen#

Watches schemas/sections/*.json and schemas/blocks/*.json. Writes:
src/__generated__/sections.d.ts
src/__generated__/blocks.d.ts
Each file declares per-type interfaces:
// auto-generated — do not edit
export interface HeroSettings {
  headline?: string;
  background_image?: string;
  alignment?: "left" | "center" | "right";
}

export interface HeroBlocks {
  cta_button: CtaButtonSettings;
}

export interface CtaButtonSettings {
  label?: string;
  href?: string;
  style?: "solid" | "outline";
}
Themes import these:
import type { HeroSettings } from "../__generated__/sections";

export default function Hero({ settings }: { settings: HeroSettings }) {
  return <h1>{settings.headline}</h1>;
}
Codegen runs at:
Plugin init (before first build)
On schema file change (HMR)
Before vite build
You can add src/__generated__ to .gitignore — it's deterministic from the schemas.

3. Dev-server middleware#

Hooks configureServer. Adds Express-style routes:
PathServes
/theme.jsThe bundle (HMR-aware in dev)
/theme.cssBuilt styles
/manifest.jsonEmitted manifest (rebuilt on schema change)
/sections.jsonSection schema index (for the customizer)
/runtime/react.js, /runtime/sdk.js, etc.Federation runtime modules so bare specifiers resolve in dev
/__numu/previewHTML shell that mounts your bundle with mock data
/assets/*Asset pipeline output
Hit http://localhost:3001/__numu/preview to see your theme rendering against a fake store with mock products, no merchant + customer context required.

4. Federation externals#

react, react-dom, react/jsx-runtime, react-dom/client, @numueg/theme-sdk are auto-added to rollupOptions.external. The plugin warns if you've added them explicitly (redundant but harmless).
In dev, the plugin's middleware serves these as ESM modules; in prod, the storefront's import map resolves them to the runtime endpoints.

5. Asset pipeline#

assets/* files are content-hashed and emitted to dist/assets/<basename>.<hash>.<ext>. A dist/asset-manifest.json maps logical names to hashed URLs:
{
  "hero.jpg":  "hero.a1b2c3.jpg",
  "logo.svg":  "logo.d4e5f6.svg"
}
The SDK's assetUrl("hero.jpg") reads this manifest at runtime to resolve URLs.

6. Manifest emission#

dist/manifest.json summarizes the build:
{
  "name": "fashion-pro",
  "version": "1.2.0",
  "main": "theme.js",
  "style": "theme.css",
  "schema_index": "sections.json",
  "asset_manifest": "asset-manifest.json",
  "error_template_url": "error.html",
  "loading_template_url": "loading.html",
  "sdk_version_required": "^0.6.0",
  "built_at": "2026-05-11T14:32:00.000Z",
  "files": [
    { "path": "theme.js",   "size": 14823, "sha256": "..." },
    { "path": "theme.css",  "size":  3201, "sha256": "..." },
    ...
  ]
}
The marketplace uses this for integrity checks at submit time.

Plugin options#

numuTheme({
  themeRoot?: string;        // default "."
  schemaDir?: string;        // default "schemas"
  localeDir?: string;        // default "locales"
  assetDir?: string;         // default "assets"
  outDir?: string;           // default "dist"
  manifestPath?: string;     // default "theme.json"
  generatedDir?: string;     // default "src/__generated__"
  strictContract?: boolean;  // default true — fail on any contract violation
  emitMockPreview?: boolean; // default true — serves /__numu/preview in dev
  schemaWatchMs?: number;    // debounce for codegen, default 100
})
Most themes use defaults. Override themeRoot only for monorepos where the theme isn't at the project root.

Custom virtual modules#

The plugin exposes one virtual module:
import { sectionRegistry } from "virtual:numu/section-registry";
The registry maps section type strings to imported components. Useful for themes that dispatch sections dynamically:
import { sectionRegistry } from "virtual:numu/section-registry";

function Renderer({ section }) {
  const Component = sectionRegistry[section.type];
  if (!Component) return null;
  return <Component settings={section.settings} blocks={section.blocks} blockOrder={section.block_order} />;
}
The plugin builds the registry from src/sections/*.tsx at config time. Adding a new file to src/sections/ auto-registers it (re-run vite after creating).

What the plugin does NOT do#

It doesn't run numu-theme submit. That's the CLI's job.
It doesn't inject CSS variables — that's the theme's responsibility (use the customizer's color settings, e.g. style={...settings.color_brand} to pass them through to JSX).
It doesn't host a customizer preview server — that's the merchant hub's job. The plugin only serves a mock preview at /__numu/preview for local checks.

When you might bypass it#

You shouldn't. The plugin is the contract — without it, your bundle won't load on the storefront.
The only legitimate reasons to deviate are:
Internal experiments not destined for the marketplace
Building a non-theme NUMU-app (Phase 9 platform)
For both, talk to the platform team first.
Modified at 2026-09-19 15:52:50
Previous
Section library
Next
API proxies
Built with