Skip to main content

Configuration

evjs has one application authoring model for SPA and MPA:

  • src/pages/**/page.* is the canonical Page and client-route anchor;
  • its containing directory determines Page scope and URL;
  • routing.mode chooses SPA or MPA materialization without changing the semantic Page/Route tree.
import { defineConfig } from "@evjs/ev";

export default defineConfig({
routing: {
mode: "spa",
},
});

The file tree supplies the rest:

src/pages/
├── page.tsx # /
├── users/
│ ├── page.tsx # /users
│ └── $userId/
│ └── page.tsx # /users/:userId
└── (account)/
└── settings/
└── page.tsx # /settings

File-convention Discovery

File conventions are enabled by default. Applications that own all runtime composition can disable filesystem discovery with the single top-level switch:

export default defineConfig({
conventions: false,
});

This disables page.* discovery under src/pages, api.* server request-route discovery under src/apis, and both global and route-scoped middleware file discovery. It cannot be combined with an explicit client routing declaration. There are no narrower convention disable switches.

SPA-only application.routes configuration does not depend on file conventions. Neither do reachable modules marked with "use server"; nor modules emitted through plugin contributions.

Routing

routing enables the canonical client Page-and-Route convention.

FieldMeaning
mode"spa" or "mpa". This changes materialization only.
htmlShared HTML template. Defaults to ./index.html.
mountShared mount selector. Defaults to #app.

Canonical Page discovery always reads src/pages; routing has no client root override.

Declare the mode explicitly so an unrelated src/pages directory is never mistaken for a framework route tree:

export default defineConfig({
routing: { mode: "spa" },
});

Page and path rules

Each route directory may contain exactly one of page.ts, page.tsx, page.js, or page.jsx. The Page module default-exports its component. The directory is also the Page-private ownership scope.

Directory segmentSemantic route segment
usersStatic users.
$userIdDynamic :userId.
$...splatTerminal catch-all.
(account)Pathless group.

The build rejects malformed segments, multiple Page extension variants, duplicate normalized paths, ambiguous dynamic parameter shapes, and generated route-id collisions. index.* has no canonical client-route meaning and can be used as an ordinary private module.

Page routes with children

Directory nesting creates child routes. In SPA mode, a parent Page can render the nested outlet:

import { Outlet } from "@evjs/ev/navigation";

export default function UsersPage() {
return (
<main>
<h1>Users</h1>
<Outlet />
</main>
);
}

A directory without page.* can organize descendants. A (group) directory also omits its own URL segment.

SPA And MPA

The same file tree changes materialization according to routing.mode.

Model objectSPAMPA
Pagepage.* with its containing-directory scopeThe same Page and scope
RouteClient Route in one browser route treeThe same semantic Route, used to select an independent Page entry
DocumentApplication-owned shell plus Page-owned Documents for static SSG PagesOne Page-owned HTML Document per static Page route
Source pathRoute directory relative to src/pagesThe same source path

SPA

export default defineConfig({
routing: { mode: "spa" },
});

SPA supports nested routes, dynamic parameters, splats, and file-convention layouts/boundaries.

MPA

export default defineConfig({
routing: { mode: "mpa" },
});

MPA discovery accepts the same page.* anchors and produces the same semantic Page/Route identities. It accepts only static Page paths: $param, terminal $...splat, and router-only boundaries fail during graph validation. Layouts compose in both modes. These errors do not activate another authoring model. A colocated index.html supplies that MPA Page's Document template.

Application Plugin Configuration

Install and configure an Application plugin with one factory call in the top-level plugins array:

import { defineConfig } from "@evjs/ev";
import { analytics } from "@company/evjs-plugin-analytics";

export default defineConfig({
routing: { mode: "spa" },
plugins: [
analytics({
endpoint: "/events",
debug: false,
}),
],
});

The factory both installs the plugin and supplies its Application configuration. Its argument is typed directly by the plugin package, so there is no second namespace, registration call, or configuration object to keep in sync. Conditional entries may use false, null, or undefined; inactive entries are omitted at runtime. Because they are not guaranteed to install, entries with a possible falsy branch do not expose plugin ids to Page config. When the Page contract has defaults, use plugin.forPages(options) to keep the plugin and its Application options active while every Page opts in explicitly.

Application configuration may contain typed executable options or explicit module references when the plugin contract allows them. Do not put secrets in values that the plugin projects into generated files or browser runtime.

Page Scope And Configuration

The complete Page directory is its private ownership scope:

src/pages/users/$userId/
├── page.tsx
├── page.config.ts
├── index.ts
├── model.ts
├── services.ts
└── components/
└── ProfileCard.tsx

Only page.* is the Page entry. Other files never create routes, so Page code does not need _ prefixes. “Private” describes framework discovery and plugin ownership, not a security boundary or import restriction. A descendant directory with its own page.* creates a more specific Page scope.

Optional Page-level configuration uses an adjacent page.config.ts:

import { definePageConfig } from "@evjs/ev";

export default definePageConfig({
title: "User profile",
meta: {
description: "View and manage a user profile.",
keywords: "users,profile",
viewport: "width=device-width, initial-scale=1",
"theme-color": "#ffffff",
},
render: "csr",
plugins: {
analytics: {
channel: "profile",
},
},
});

The module is evaluated synchronously while evjs constructs the build graph. It must default-export a plain object containing static JSON data. Supported core fields are title, meta, render, hydrate, prerender, and rsc. Omitting render always selects CSR, and CSR must omit hydrate. Explicit SSR and SSG Pages may use hydrate: "none" | "load"; ordinary SSR defaults to "load", SSG defaults to "none", and RSC/PPR remain unhydrated at Page level. meta is a string record for <meta name="key" content="value">; it does not accept property, charset, links, scripts, functions, or a generic head tree. Installed Page-aware plugins use their canonical ids under plugins. The Page module does not import the plugin package: ev prepare, ev dev, and ev build generate src/plugin-types.d.ts as a stable bridge to the static type of ev.config.ts. TypeScript derives plugin ids and Page values from that config type, but only for entries statically guaranteed to install. JavaScript config does not widen the Page registry to any; use ev.config.ts when Page plugin completion is required.

Application and Page configuration are independent plugin contracts. evjs does not merge the object passed to the Application factory into a Page value. Within either contract, authored fields deep-merge over that contract's defaults before validation. Within the Page map, a normal factory call uses Page defaults for an omitted plugin entry when defaults exist; otherwise omission disables that Page. Defaultable Page contracts expose forPages(), which always treats omission as disabled; non-defaultable contracts are already opt-in-only. false disables the plugin for this Page, true requires Page defaults, and an object enables it after merging over Page defaults and validation. Page objects must be strict static JSON.

The plugin API targets the same normalized Page identity in both modes. Core title/meta values are materialized for the Page in both modes. Configured Page plugin values are build-time graph data and are not automatically published to browser runtime; the plugin must explicitly generate and attach any runtime projection it needs. Plugins derive Route or Document behavior from the normalized Page instead of exposing separate Route or Document plugin configuration.

Other Configuration

Server

When file conventions are enabled, positive api.* server request-route anchors are discovered under the fixed src/apis root:

export default defineConfig({
routing: { mode: "spa" },
server: {
basePath: "/__evjs",
},
});

server.basePath owns server-function, PPR, and RSC runtime paths. It must be an absolute pathname using non-empty ASCII URL-safe segments containing only letters, digits, ., _, ~, or -. Empty and standalone . or .. segments, dynamic :param and wildcard * segments, percent escapes, and raw non-ASCII characters are not valid runtime endpoint configuration.

Server middleware conventions are:

  • src/middleware.ts for global server middleware;
  • src/apis/**/middleware.ts for middleware scoped to same-directory and descendant server file routes.

Enable React Server Components per Page with rsc: true in page.config.ts. The Flight endpoint is derived from server.basePath; optionally override it with server.rsc: { endpoint: "/custom/flight" }. server.rsc is not an enable switch, and its endpoint override follows the same absolute ASCII static pathname rule.

The server-function endpoint is exact. RSC adds another exact endpoint only when an RSC Page exists, and PPR reserves a rooted subtree only when PPR is active. The BuildPlan rejects collisions among active endpoints and between a reserved endpoint and any Page, redirect, or server request Route pattern.

Dev Server

The browser dev server defaults to port 3000; the server runtime defaults to 3001. They are preferred ports and may move together when occupied.

export default defineConfig({
routing: { mode: "spa" },
dev: {
port: 4000,
proxy: [
{
context: ["/api"],
target: "http://localhost:8080",
changeOrigin: true,
},
],
},
server: {
dev: { port: 4001 },
},
});

dev.https accepts false, true, or a { key, cert } object. The default Utoopack adapter accepts only the boolean forms and rejects an explicit certificate instead of discarding it; select the Webpack adapter when custom client-dev certificates are required. server.dev.https accepts false or an explicit { key, cert } object; the framework server does not synthesize a certificate for true.

Output

Browser assets default to dist/client; server artifacts default to dist/server. Both values must be project-relative strict descendants of the BuildPlan distDir (dist for framework commands), must use / as the portable separator, and must not contain empty, . or .. path segments. They must resolve without symbolic links to separate, non-nested directories. This keeps adapter writes and cleanup scoped to one framework-owned output tree.

export default defineConfig({
routing: { mode: "spa" },
output: {
client: "dist/public",
server: "dist/runtime",
},
});

After plugin configure() and setup() finish, the BuildPlan owns the resolved output paths. Adapters use those paths for cleanup, emitted assets, stats, and manifests; configureBundler() hooks cannot override framework-owned client or server output paths.

output.crossOriginLoading accepts false, "anonymous", or "use-credentials".

Transport

Set transport.baseUrl only when the browser calls the server runtime on a different origin:

export default defineConfig({
routing: { mode: "spa" },
transport: {
baseUrl: "https://api.example.com",
},
});

Plugins

Install plugins through plugins, normally as pluginFactory(applicationConfig). A plugin can declare an independent Page contract whose canonical id becomes available in adjacent page.config.ts files. The same Plugin descriptor owns configure(), setup(), emitIR(), and lifecycle hooks. See Plugins.

Bundler

Utoopack is the default. Supply a bundler adapter only when intentionally using the validation/fallback backend:

import { defineConfig } from "@evjs/ev";
import { webpackAdapter } from "@evjs/bundler-webpack";

export default defineConfig({
routing: { mode: "spa" },
bundler: webpackAdapter,
});

Every adapter declares build capabilities for server rendering, RSC, and PPR, plus dev-plan update capabilities for HTML, entries, routes, server output, and resolution. ev inspect reports the selected adapter and any plan gaps; build and dev fail before adapter execution when a required capability is missing.

Route Inputs

Canonical Page discovery is enabled by routing.mode. An unrelated src/pages directory is not interpreted as a route tree when routing is absent. Only page.* anchors participate in this file convention.

Explicit SPA route configuration

application and routing are mutually exclusive. application.routes accepts nested routes, page or component, layout, wrappers, redirects, and Application Document configuration through the SPA-only explicit route-tree normalizer. application.pageRoot is the Page source root for both page and component references in this explicit input; it defaults to ./src/pages and never changes canonical file discovery. A page value selects a page.*-anchored directory relative to that root. A component selects a module inside the same root: @/pages/... is a logical alias for the configured application.pageRoot, while bare and ./ component references are relative to it. Component paths and resolved symbolic links cannot escape the configured Page source root. Layout and wrapper references keep their project-source resolution semantics. The children spelling is rejected; nested declarations use routes. exact: true is accepted only as a terminal-match structural assertion; exact: false and nested routes below an exact Route are rejected, and exact is not copied into the graph. Plugin configuration is not authored on explicit Route or Document objects; Page-aware plugins derive those contributions from normalized Pages. MPA materialization, Document alias conflicts, and component references outside the Page root are rejected. Static segment identity is compared after exactly one URL decode. Raw and percent-encoded aliases therefore cannot coexist, and a segment that decodes to . or .. is rejected because WHATWG URL parsing removes it before route matching.

An explicit component ending in index.* or page.* claims its containing directory as the Page scope. A flat component such as <application.pageRoot>/403.tsx remains module-scoped so it cannot accidentally claim other flat Pages in the configured root; a module-scoped Page does not discover an adjacent page.config.ts.

Canonical Page tree

With routing.mode, each published Page lives in the directory for its public URL and uses page.*. Static title, named metadata, rendering settings, and plugin-owned Page values live in the adjacent page.config.ts. Dynamic, terminal catch-all, and pathless segments use $param, $...splat, and (group) directories. Run ev inspect to review the normalized Page/Route/Document graph.

The public configuration surface is the schema described above. Use ev inspect to review normalized Page, Route, source, Document, and diagnostic information.