Ionify is intentionally config-light. The goal is: a small, explicit config surface that maps to real pipeline behavior (graph versioning, CAS isolation, deps optimizer, federation contracts).
ionify.config.ts
Ionify looks for one of:
ionify.config.tsionify.config.mtsionify.config.js/ionify.config.mjs/ionify.config.cjs
You can export an object or use defineConfig:
import { defineConfig } from "@ionify/ionify";
export default defineConfig({
entry: "/src/main.tsx",
});
create-ionify
Scaffold a new project (optionally monorepo) via:
npm create ionify@latest
pnpm create ionify@latest
yarn create ionify
bunx create-ionify@latest
Common CLI flags:
--template basic|dashboard--monorepo/--no-monorepo--testing vitest|playwright|both--pm pnpm|npm|yarn|bun--yes(defaults)
Workspace identity (workspaces + monorepos)
Ionify automatically discovers the workspace root (pnpm/yarn/npm workspaces + Git submodules) and unifies state under the workspace .ionify/ directory.
Ionify also sets these environment variables for engine components and tooling:
IONIFY_WORKSPACE_ROOTIONIFY_PROJECT_ROOTIONIFY_STATE_DIRIONIFY_WORKSPACE_IDIONIFY_PROJECT_ID
Scaffolding extras
The scaffolder can optionally configure:
- AI assistant setup:
.cursorrules,.github/copilot-instructions.md, anddocs/ai-prompts/ - Performance budget enforcement: Lighthouse config + CI workflow + a
performanceBudgetsection inionify.config.ts - Visual regression testing: Percy/Chromatic setup + example tests + CI workflow
performanceBudget
When enabled by the scaffolder, performanceBudget is used by the generated CI/scripts to fail builds when budgets are exceeded.
import { defineConfig } from "@ionify/ionify";
export default defineConfig({
// Example (preset-based)
performanceBudget: "strict", // "moderate" | "relaxed" | false
});
Generated files typically include:
lighthouserc.json.github/workflows/performance.ymlscripts/analyze-bundle.js
Visual regression testing
When enabled by the scaffolder, visual tests run on CI and catch UI diffs before merge.
Generated files typically include:
tests/visual/.github/workflows/visual-tests.ymlpercy.config.js(or Chromatic config)
DX metrics
Ionify build/dev output reflects real engine state (CAS-first hydration + deterministic planning).
ionify buildprints: Modules in plan, CAS hits, transforms needed, and total time.ionify analyzesummarizes.ionify/state (graph + packs + slimming when enabled).
root
root sets the project root directory Ionify uses for:
- Resolving
entryand imports - Locating env files (
.env*) - Storing caches under
.ionify/(CAS + deps artifacts)
Notes:
- In a monorepo/workspace, Ionify stores
.ionify/at the workspace root (shared across apps).
Defaults:
- If you have an
ionify.config.*, Ionify uses the config file’s directory - If you don’t, Ionify uses
process.cwd()
Entry
entry: string or string[] (project-relative,/src/...is supported)
This feeds the graph and the build planner.
Resolve
export default {
resolve: {
alias: {
"@core": "/core",
},
extensions: [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json"],
conditions: ["import", "module", "browser", "default"],
mainFields: ["module", "jsnext:main", "jsnext", "main"],
},
};
optimizeDeps (deps optimizer + packs)
Ionify’s deps optimizer serves node_modules through /@deps/* and caches deterministic artifacts under .ionify/deps/<depsHash>/.
Core options:
optimizeDeps.include: string[]Pre-optimize these on dev server start (background pre-warm).optimizeDeps.exclude: string[]Skip optimization and pack selection for these deps.optimizeDeps.bundleEsm: boolean(defaulttrue) Bundle ESM deps into self-contained files to reduce request waterfalls.optimizeDeps.sourcemap: boolean(defaultfalse) Sourcemaps for optimized deps.
sharedChunks
optimizeDeps.sharedChunks: "auto" | booleanBuild shared chunks when optimizing multiple dep entries as one graph.
Notes:
- Vendor packs require
sharedChunks !== falseto be effective. - For best request reduction, keep
bundleEsm=trueandsourcemap=false.
vendor (preloader)
optimizeDeps.vendor: "auto" | string[] | falseBuild avendor.<depsHash>.jspreloader module to start fetching hot deps earlier.
This can improve cold-start waterfalls, but it does not collapse request count by itself.
vendorPacks (few-request mode)
vendorPacks is the real “few-request mode”: it bundles many deps into a small number of pack files, then Ionify rewrites imports to route through those packs.
Modes:
falseDisable packs (default)trueForce one heuristic “app vendor” pack"auto"Progressive layered vendor: build core first, then lazy-build feature packs in the background when the graph proves they’re needed{ [packName]: string[] }Manual packs
Auto-selection caps:
optimizeDeps.vendorPackMaxBytes: number(default: 600KB)optimizeDeps.vendorPackMaxMembers: number(default: 25)
packSlimming (usage-driven)
optimizeDeps.packSlimming: "auto" | boolean
When enabled, Ionify writes a deterministic usage index (deps-usage.v1.json) and builds usage-minimized pack variants in the background.
On warm reload, Ionify prefers the slim pack if it’s ready, otherwise falls back to the base pack or /@deps/* wrappers.
Example (recommended progressive config):
import { defineConfig } from "@ionify/ionify";
export default defineConfig({
entry: "/src/main.tsx",
optimizeDeps: {
sharedChunks: "auto",
vendorPacks: "auto",
packSlimming: "auto",
// Optional helpers
vendor: "auto",
include: ["lodash"],
},
});
Example (manual packs: core/ui/data):
import { defineConfig } from "@ionify/ionify";
export default defineConfig({
entry: "/src/main.tsx",
optimizeDeps: {
sharedChunks: true,
vendorPacks: {
core: [
"react",
"react-dom/*",
"scheduler",
"scheduler/*",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"react-router",
"react-router-dom",
"@remix-run/router",
"react-refresh",
"react-refresh/*",
],
ui: ["@mui/*", "@radix-ui/*"],
data: [
"@tanstack/react-query",
"@tanstack/react-query/*",
"axios",
"axios/*",
"zod",
"zod/*",
"react-hook-form",
"react-hook-form/*",
"@hookform/resolvers",
"@hookform/resolvers/*",
"zustand",
"zustand/*",
],
},
packSlimming: "auto",
},
});
Federation
federation lets Ionify connect independently built apps without turning every app into a duplicate vendor bundle.
Core options:
federation.host: stringStable app identity written into the build manifest. Defaults topackage.json#nameor the project directory name.federation.remotes: Record<string, string | RemoteConfig>Remote apps this host can import from.federation.exposes: Record<string, string>Local modules this app exposes to other hosts.federation.shared: Record<string, boolean | SharedConfig>Dependencies that should be treated as shared contracts instead of repeated payload.
Remote config:
entry: stringRemote manifest/container entry URL.external?: string | string[]Import specifier(s) Ionify preserves in source and production output. Defaults to the remote name.version?: stringExpected remote version.integrity?: stringOptional integrity metadata for the remote entry.hash?: stringOptional remote contract/content hash.
Shared config:
singleton?: booleanUse one runtime instance, usually for React and React DOM.requiredVersion?: stringVersion range this app expects from the shared dependency.version?: stringVersion this app provides.strictVersion?: booleanFail closed when the shared contract does not match.eager?: booleanInclude the shared module eagerly when that is the right runtime tradeoff.shareScope?: stringIsolate shared contracts by scope.
Example (host consuming a remote and sharing React once):
import { defineConfig } from "@ionify/ionify";
export default defineConfig({
entry: "/src/main.tsx",
federation: {
host: "shell",
remotes: {
dashboard: {
entry: "https://dashboard.example.com/ionify-federation.json",
external: ["dashboard/", "dashboard"],
version: "1.4.0",
},
},
shared: {
react: { singleton: true, requiredVersion: "^19.0.0" },
"react-dom": { singleton: true, requiredVersion: "^19.0.0" },
},
},
});
Example (remote exposing modules to hosts):
import { defineConfig } from "@ionify/ionify";
export default defineConfig({
entry: "/src/main.tsx",
federation: {
host: "dashboard",
exposes: {
"./Widget": "/src/federated/Widget.tsx",
"./routes": "/src/federated/routes.ts",
},
shared: {
react: { singleton: true, version: "19.0.0" },
"react-dom": { singleton: true, version: "19.0.0" },
},
},
});
What this saves:
- The host imports remote code through a stable external contract instead of bundling the remote app into the host.
- Shared dependencies such as React are declared once as runtime contracts, so the host and remote do not each ship and initialize their own copy.
- Federation metadata is folded into Ionify's graph/CAS model, so changed remote contracts invalidate precisely instead of forcing unrelated rebuild work.
Env handling (Vite-compatible)
Ionify loads env files (Vite-style):
.env.env.local.env.<mode>.env.<mode>.local
envPrefix
envPrefix controls which env vars Ionify is allowed to expose to the browser via import.meta.env.*.
- Default:
["VITE_", "IONIFY_"] - Ionify always exposes:
MODE,NODE_ENV, plusDEV/PROD
Ionify auto-exposes env vars to the client as import.meta.env.* when they match envPrefix.
You can also declare types in your app (example from react-basic/src/ionify-env.d.ts):
declare global {
interface ImportMetaEnv {
readonly MODE: string;
readonly NODE_ENV: string;
readonly DEV: boolean;
readonly PROD: boolean;
readonly IONIFY_API_URL: string;
}
}
define (compile-time constants)
export default {
define: {
__APP_VERSION__: JSON.stringify("1.0.0"),
"process.env.NODE_ENV": JSON.stringify("development"),
},
};
Engine-level flags (optional)
Ionify also supports env flags for pipeline selection:
IONIFY_PARSER=oxc|swc|hybridIONIFY_MINIFIER=oxc|swc|autoIONIFY_OPTIMIZATION_LEVEL=0|1|2|3
Output directory (today)
Build output defaults to dist/. Override via CLI:
ionify build --out-dir dist