# Configuration: `zoneConfig`
```ts
import { zoneConfig } from "@runsnip/next-zones/config";
export default zoneConfig({ ...zone, ...nextConfig }); // one object
export default zoneConfig(zone, (phase) => nextConfig); // a Next config that is a function of the phase
```
One object holds both: `zoneConfig` takes out the zone's keys below and passes every other key to Next. A Next config
that is a function of the phase comes second, after the zone's keys, so the declaration is known without calling it.
`zoneConfig(zone, nextConfigObject)` is read too. In both two-argument forms, Next keys left in the first object are
kept, and the second wins on a key both set.
## `zone`
| Field | Type | Required | Meaning |
|---|---|---|---|
| `mount` | `string` | yes | `"/"` for the shell, otherwise one URL segment such as `"/blog"` (lowercase letters, digits, `-`) |
| `aliases` | `{ source: string; destination: string }[]` | no | URLs at the root that this zone serves. See [aliases](aliases.md) |
| `endpoints` | `{ base?, events?, health?, admin? }` | no (the shell only) | The URLs Zones serves of its own, under `base` (`"/_next-zones"` by default); none unless declared. See [endpoints](zones.md#endpoints) |
| `mode` | `"zones"` \| `"single"` | no (the shell only), `"zones"` | How the workspace is served. `"zones"`: the shell is built as an app and every other zone as an [image](zones.md#zone-images), and Zones installs images while it runs. `"single"`: every zone is linked into one Next app, run by `next start`, with no live installs. See [build and start](cli.md#build-and-start). `output` and Next's other options stay Next's keys, in the same object |
| `metrics` | `boolean` | no (the shell only), `false` | Opens one metrics store for the whole Zones process: requests by zone and version, installs, pulls, memory, and what a zone's code writes with `@runsnip/next-zones/metrics`; served as Prometheus text at `/metrics` to an admin. Off, the metrics functions do nothing. See [metrics](metrics.md) |
| `livePull` | `boolean` | no, `false` | Whether a ping (an admin request to Zones' `/images///pull` or `/install`) may make Zones pull this zone's images from its sources. Recorded in each zone image's `zone.json`; read from the zone's latest image before anything is fetched. Without it, the zone's images are pulled on the server side only (`next-zones pull`, a deploy step). Images already in the store install either way. See [live pulls](zones.md#live-pulls) |
An invalid declaration throws as soon as `next.config` is loaded, for example: `mount must be "/" (the shell) or one
URL segment like "/blog"`.
## `nextConfig`
The zone's own Next config:
- every key of the object that is not the zone's;
- or a function of the phase, `(phase, { defaultConfig }) => config`, which may be async, as the second argument: what
it returns is laid over the first object's Next keys, key by key at the top level.
`zoneConfig` returns it with the build options below, the zone declaration (attached under a symbol that Next
ignores and next-zones reads back), and, when the zone runs alone, its aliases as rewrites. Everything else in your
config is passed through unchanged.
## Next's options are yours
`zoneConfig` passes the zone's Next config through as it is, with one exception: in a build made by `next-zones build`,
it fills in the [build options for Zones](#build-options-for-zones) below, and nothing else. `output`, `images`,
`experimental` and the rest mean what they mean in Next, in every mode. A plain `next build` or `next dev` gets the
config untouched.
## Build options for Zones
Zones runs zones that were built separately, sharing one instance of each module they have in common. That needs
three Turbopack options off in the builds Zones runs (the shell and every zone image). `zoneConfig` fills them in
**only in `next-zones build`** (the shell and every zone image, in both modes: `"single"` links the same images), and
**only where your config leaves them unset**. Anywhere else (a zone built alone with `next build`, `next-zones dev`)
nothing is filled in.
If your config sets one of them otherwise, a build for Zones stops with an error naming it, rather than overriding it.
Setting them to `false` yourself is allowed, and changes nothing. `turbopack.root` and `outputFileTracingRoot` you may
set too: what counts is that the shell and every zone are built from one root.
Zones refuses a shell or a zone image that was built without them, and says to build it with `next-zones build`.
| Option | In a build for Zones | Why | Cost (measured) |
|---|---|---|---|
| `experimental.turbopackScopeHoisting` | `false` | Hoisting merges modules into one factory that writes other modules' exports. A module shared by several builds is loaded once, so no build may write into it | Server JS of the test shell +16% (520 → 604 KB); client JS unchanged |
| `experimental.turbopackRemoveUnusedExports` | `false` | Turbopack drops the exports a build does not use. A library used by the shell and a zone then differs between their builds: the shell's copy has only what the shell uses. They load as two modules, so a context it creates exists twice, and a zone's hook does not see the shell's provider (found with `@tanstack/react-query`: "No QueryClient set") | Two real apps: client JS +1.9% and +3.4%, server JS +8.6% and +4.4% |
| `experimental.turbopackRemoveUnusedImports` | `false` | Turbopack refuses to build with it on while unused exports are kept | Included above |
### The project root
Turbopack names every module by its path from the project root, and its module ids come from that name, so the shell
and every zone image must be built from one root to share modules. Next infers the root from the topmost lockfile
above the app: a lockfile added in a parent folder would move it, and rename every module of the builds after it. A
build for Zones therefore pins it: `turbopack.root` and `outputFileTracingRoot` are set, when your config leaves them
unset, to `NEXT_ZONES_ROOT`, or else to the nearest folder whose `node_modules` holds `next` (widened to hold that
`node_modules` where it really is, when it is a link). Zones refuses a zone image built from another root than the
shell's, and says so.
Mode `"single"` links the same zone images into one app, where a module several builds use is also loaded once, so it
needs them too.
The cost is the size in the last column, and nothing else. Getting it back means sharing a module by what it exports,
not by its whole code (debt D8 in the README). That was tried and measured: splitting a shared library into a
fragment per export, or pinning only the modules that hold state, came back larger or not exact. What stops it is
Turbopack itself: unused-export removal is one switch for the whole build, with no way to keep one module's exports
whole. So the options stay off.
## Running a zone alone
A zone is a normal Next app: it builds and runs on its own (`next build`, `next start`, `next dev`), and can be
deployed on its own domain. Alone, it serves its own routes, and `zoneConfig` adds its aliases to its own rewrites.
A build for Zones (`next-zones build`) leaves them out, because Zones serves them.
Links to the other apps are then links to another site: point them at where those apps live.
## Example
```js
import { zoneConfig } from "@runsnip/next-zones/config";
export default zoneConfig({
mount: "/blog",
aliases: [{ source: "/post/:id", destination: "/blog/:id" }],
transpilePackages: ["shared"],
reactStrictMode: true,
});
```
## `readZone(dir)`
```ts
import { readZone } from "@runsnip/next-zones/config";
const zone = await readZone("./blog"); // { mount: "/blog", aliases: [...] }, or null if not a zone
```
It loads the app's `next.config.mjs`, `.js`, `.ts` or `.mts`, and returns its declaration. Tools use it: the CLI,
build scripts, Zones.