Build
Commands
ev inspect
ev inspect --json
ev prepare
ev build
ev inspectvalidates and reports framework inputs without writing.evordist.ev preparewrites generated framework IR under.evwithout running the bundler.ev buildresolves config, builds the graph and plan, runs the selected bundler, links build facts, and writes production output.
ev prepare, ev build, and ev dev share one per-project operation lock.
Starting a second output-mutating command for the same project fails with the
active operation and process ID instead of racing writes to .ev, route types,
dist, or deployment artifacts. Different project directories remain
independent.
Inspect
For a canonical application, the routing summary uses the public
Page-and-Route vocabulary: mode, Page root, discovered page.* anchors,
directory-derived route patterns, Documents, and diagnostics.
Canonical inspect output does not present a provider, resolver implementation, or route-types path. It reports resolved Pages, Routes, Documents, server functions, server routes, rendering metadata, installed plugin settings, Page config sources, provenance, and diagnostics. Errors make inspect exit non-zero.
Generated IR
ev prepare writes .ev, including:
- normalized CoreGraph;
- generated framework and plugin modules;
- entry facades and framework slots;
- import edges;
- final BuildPlan;
- manifest inputs and provenance.
Canonical applications write the validated semantic graph to
.ev/framework/core-graph.json. .ev is generated and must not be edited.
Output
By default browser and server files are separated:
dist/
├── client/
│ ├── index.html
│ ├── main.[hash].js
│ └── [chunk].[hash].js
├── server/
│ └── main.[hash].js
└── deployment-metadata.json
Use output.client and output.server when the host requires another layout.
Both directories must use portable /-separated project-relative paths with no
empty, ., or .. segments. They must remain separate, non-nested, symlink-free
strict descendants of the BuildPlan distDir:
export default defineConfig({
routing: { mode: "spa" },
output: {
client: "dist/public",
server: "dist/runtime",
},
});
The finalized BuildPlan is the single source of truth for adapter cleanup,
emitted assets, stats, and manifest paths. A plugin configureBundler() hook may
change supported low-level bundler settings, but it cannot override a
framework-owned client or server output path.
Bundler server facts use serverEntryAssets, keyed by each exact server
BuildPlan entry name. Every server entry must emit exactly one self-contained
JavaScript asset. When a bundler reports a complete server asset inventory,
that inventory must contain each declared entry asset and no additional
unowned JavaScript chunks; Core never infers server ownership from module
stats or filenames.
Generated HTML embeds the ClientRuntime required by browser bootstrap.
deployment-metadata.json is the canonical serialized deployment projection;
the complete BuildOutput remains in memory. Application code must not import
or edit deployment metadata.
SPA And MPA Output
routing.mode controls route and Document materialization:
| Routing mode | Route output | Document output |
|---|---|---|
spa | Client Routes in one browser route tree | One Application-owned shell, plus a Page-owned output for each static SSG Page |
mpa | Independent Page entries for static semantic routes | One Page-owned Document per static Page route |
Both use the same src/pages/**/page.* entry, directory scope, and
semantic route pattern.
Static SSG Pages use their semantic route as the output path in either mode:
/ writes index.html, while /report writes report/index.html. The output
is never derived from the Page id. When a root SSG Page owns index.html in a
mixed SPA that also needs a client-route fallback, Core keeps the Application
shell separately at __evjs/<application-id>.html.
MPA materializes only static Page routes. $param and terminal
$...splat remain valid SPA route identities, but selecting MPA for either
fails graph validation because one dynamic pattern does not identify one
build-time HTML output. Route layouts compose in both modes; router-only
boundary facets remain SPA-only and MPA rejects them explicitly.
Place index.html beside an MPA Page when it needs a Page-specific Document
template:
src/pages/report/
├── page.tsx
└── index.html
Canonical SPA/MPA Pages both discover an optional page.config.ts from their
Page directory:
import { definePageConfig } from "@evjs/ev";
export default definePageConfig({
title: "Report",
meta: {
description: "A generated business report.",
keywords: "report,analytics",
viewport: "width=device-width, initial-scale=1",
"theme-color": "#ffffff",
},
render: "ssr",
hydrate: "load",
plugins: {
analytics: {
channel: "report",
},
},
});
The module is synchronously evaluated at graph-build time. Core rendering
fields flow into the rendering BuildPlan. For emitted MPA/SSG Documents and
compiled SSR/PPR/RSC request-time document shells, static title and named
meta materialize missing tags and override matching template baseline
values; omitted values preserve the baseline. Page plugin settings remain
static graph data unless the owning plugin explicitly projects them into a
generated runtime artifact. Plugin transformHtml hooks run after
framework metadata, assets, and structured HTML contributions materialize and
may explicitly override the result.
For every server-rendered Page, evjs compiles its configured HTML template into
a request-time document shell during the build. This preserves authored
<html>, <head>, and <body> attributes and content while applying the same
assets, Page metadata, html.tag contributions, and transformHtml hooks as a
static Document. The default React renderer inserts the Page HTML and
request-specific bootstrap data into that shell.
Supplying a custom renderDocument completely replaces the compiled shell:
ctx.page.metadata remains available, but the custom renderer owns the
template baseline, assets, and document structure. Insert
renderReactPageMetadata(ctx) from @evjs/server/react to retain the core
safe-serialization and SPA cleanup behavior. Build-time transformHtml hooks
do not post-process the arbitrary per-request string returned by a custom
document renderer.
Page Rendering Settings
Page components do not export literal render, hydrate, prerender, or
rsc settings. Put those values in adjacent page.config.ts:
import { definePageConfig } from "@evjs/ev";
export default definePageConfig({
render: "ssr",
hydrate: "none",
prerender: { partial: true },
});
Static generation uses the supported "ssg" rendering contract. RSC and
partial-prerendered Pages must omit hydrate or set it to "none". RSC Pages
use render: "ssr" and rsc: true; their Flight endpoint is derived from
server.basePath unless server.rsc.endpoint overrides it. Both runtime path
settings must use non-empty ASCII URL-safe segments containing only letters,
digits, ., _, ~, or -. Empty and standalone . or .. segments,
:param, *, percent escapes, and raw non-ASCII characters are rejected.
RSC and partial prerendering cannot be combined on one Page. These settings
normalize to Core Page rendering fields without changing Page identity.
Omitting render always normalizes the Page to "csr". CSR mounts a new
client tree and therefore must omit hydrate; only explicitly selected SSR or
SSG Pages can configure hydrate: "load" | "none". Ordinary SSR defaults to
load hydration, SSG defaults to no hydration, and RSC/PPR remain unhydrated at
the Page level. Generated runtime metadata still uses an effective "load"
client activation for CSR bootstrap, but that internal value is not a Page
authoring option.
The server-function and active RSC endpoints are exact paths. An active PPR
endpoint owns its rooted subtree. Build planning requires those active
endpoints to be disjoint and rejects any Page, redirect, or server request
Route pattern that can match a reserved runtime path. server.basePath derives
default endpoints but does not reserve a request subtree of its own; dev and
generated Node/Edge deployment routing preserve that distinction.
Server Functions And Routes
Reachable modules beginning with "use server"; contribute supported named
server functions.
Server request Routes are discovered independently from positive api.*
anchors under src/apis:
// src/apis/api/health/api.ts
export const GET = async () => Response.json({ ok: true });
Build Checks
Check user-controlled inputs first:
ev.config.tsdeclaresrouting.mode;- every published client Page uses exactly one
page.*extension variant; - each Page uses at most one
page.config.tsorpage.config.js, whose default export is static JSON data; - Page entries default-export a component;
- route directories use valid static,
$param, terminal$...splat, and(group)segments without normalized-path conflicts; - MPA does not use unsupported dynamic paths or router-only boundary facets;
- templates contain the configured mount element;
- Page
titleand eachmetaname/content value are valid static strings; - Page rendering metadata in
page.config.tsuses supported values and combinations; "use server"modules begin with the directive and export named callables;- each published server request Route uses exactly one
api.*extension variant in its URL directory; api.*anchors export uppercase HTTP methods only.- every URL-owning client Route (Page or redirect) is disjoint from server
request Route patterns, including across static, dynamic, and terminal splat
matches. Static aliases are compared after exactly one URL decode, so
/%75sersaliases/userswhile double-encoded text remains distinct.
Before building, run ev inspect and review Page sources, Page config, routes,
Documents, provenance, and diagnostics.
Key Points
- SPA and MPA apps build from the same
page.*Page-and-Route tree. ev inspectreportsroutingMode, Page root, source, and Document defaults without exposing an internal provider choice..ev, manifests, build output, and generated route-type declarations are generated.- Bundler adapters consume BuildPlan as the source of routing, runtime, and output ownership, then return build facts.
- When stats expose a reliable complete physical inventory, adapters return it
through
BundlerBuildFacts.emittedFiles. Each reported side is complete; an omitted client or server side means unknown, never an empty output.