@numueg/theme-plugin — added to every theme's vite.config.ts.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",
},
},
},
});buildStart + closeBundle. On buildStart, parses src/main.tsx and asserts:mount existsmount is a function@numueg/theme-sdk appear in rollupOptions.externalcloseBundle, scans the emitted theme.js and asserts no copy of React snuck in (greps for react.production.min's telltale strings).schemas/sections/*.json and schemas/blocks/*.json. Writes:src/__generated__/sections.d.ts
src/__generated__/blocks.d.ts// 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";
}import type { HeroSettings } from "../__generated__/sections";
export default function Hero({ settings }: { settings: HeroSettings }) {
return <h1>{settings.headline}</h1>;
}vite buildsrc/__generated__ to .gitignore — it's deterministic from the schemas.configureServer. Adds Express-style routes:| Path | Serves |
|---|---|
/theme.js | The bundle (HMR-aware in dev) |
/theme.css | Built styles |
/manifest.json | Emitted manifest (rebuilt on schema change) |
/sections.json | Section schema index (for the customizer) |
/runtime/react.js, /runtime/sdk.js, etc. | Federation runtime modules so bare specifiers resolve in dev |
/__numu/preview | HTML shell that mounts your bundle with mock data |
/assets/* | Asset pipeline output |
http://localhost:3001/__numu/preview to see your theme rendering against a fake store with mock products, no merchant + customer context required.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).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"
}assetUrl("hero.jpg") reads this manifest at runtime to resolve URLs.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": "..." },
...
]
}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
})themeRoot only for monorepos where the theme isn't at the project root.import { sectionRegistry } from "virtual:numu/section-registry";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} />;
}src/sections/*.tsx at config time. Adding a new file to src/sections/ auto-registers it (re-run vite after creating).numu-theme submit. That's the CLI's job.style={...settings.color_brand} to pass them through to JSX)./__numu/preview for local checks.