# Config files and layering (/docs/config-files)

## File names [#file-names]

tyfigo claims files whose name ends in `.config.<ext>`: `app.config.yaml`, `server.config.toml`, `jvm.config.properties`, and
the files of a [`.config` directory](#a-config-directory). A plain `data.yaml` anywhere else is left alone, so it can belong
to another content mapper or bundler loader.

The part before `.config` is the stem. Every file that shares a stem is a layer of the same config:

| File                               | Layer             | Read when                                               |
| ---------------------------------- | ----------------- | ------------------------------------------------------- |
| `app.config.yaml`                  | base              | always                                                  |
| `app.production.config.yaml`       | environment       | the environment is `production`                         |
| `app.local.config.yaml`            | local             | always (not in browser builds)                          |
| `app.production.local.config.yaml` | environment-local | the environment is `production` (not in browser builds) |

You always import the base file. The other layers are found from its name.

## Order [#order]

Layers apply in this order, each one over the result of the ones before it:

1. `app.config.yaml`
2. `app.<environment>.config.yaml`
3. `app.local.config.yaml`
4. `app.<environment>.local.config.yaml`
5. Environment variables, through [placeholders](/docs/placeholders)

Environment files sit next to the base file. Local files are looked for next to the base file first, then in the project root
(the nearest folder with a `package.json`). Keep local files out of version control:

```txt title=".gitignore"
*.local.config.*
.env.local
```

## A `.config` directory [#a-config-directory]

Instead of the `.config` suffix, you can keep config files in a folder named `.config`. A file directly inside it, with one of
the [format](/docs/formats) extensions (`.yaml`, `.yml`, `.toml`, `.jsonc`, `.json5`, `.ini`, `.properties`, `.xml`), is a
config file. The layers follow the same naming, without the `.config` part:

| File                                | Layer             |
| ----------------------------------- | ----------------- |
| `.config/app.yaml`                  | base              |
| `.config/app.production.yaml`       | environment       |
| `.config/app.local.yaml`            | local             |
| `.config/app.production.local.yaml` | environment-local |

```ts title="src/main.ts"
import config from '../.config/app.yaml';
```

- Environment files sit next to the base file, in the same `.config` folder.
- Local files are looked for next to the base file first, then in the project root's `.config` folder.
- `.env` files are looked for in the folder that contains `.config` first, then in the project root, so `.env` sits where it
  usually does, not inside `.config`.
- Files in folders below `.config` (`.config/db/app.yaml`) are not config files.

### Running it [#running-it]

Nothing to set up. `tyfigo/register`, `tyfigo/bun`, `tyfigo/jest` (with the pattern on [Jest](/docs/runtimes/jest)) and every
bundler plugin load `.config/*.yaml` and the other extensions by their path. A `.yaml` file anywhere else is still left alone:
Bun loads it with its own loader, Node.js still refuses it, and bundlers handle it as they did before.

### Type checking is opt-in [#type-checking-is-opt-in]

TypeScript picks a content mapper by extension, and `.config/app.yaml` ends in `.yaml`, not `.config.yaml`. To type it, give
tyfigo the plain extensions you use in `.config` as well, and include the folder as `.config/*`:

```jsonc title="tsconfig.json"
{
	"contentMappers": [
		{
			"package": "tyfigo",
			"extensions": [
				".config.yaml",
				".config.yml",
				".config.toml",
				".config.jsonc",
				".config.json5",
				".config.ini",
				".config.properties",
				".config.xml",
				".yaml",
			],
		},
	],
	"include": ["src", ".config/*"],
}
```

TypeScript skips folders whose name starts with a dot when it expands a folder in `include`, so `".config"` alone only checks
the files your code imports. `".config/*"` names the files and checks the environment and local files too.

<Callout type="warn" title="tyfigo then owns every .yaml file">
	Each extension has one content mapper, so you can't also give `.yaml` to another mapper, such as
	[Motherload](/docs#tyfigo-and-motherload). Every other `.yaml` file your code imports, such as `fixtures.yaml`, becomes
	**plain data**: typed widened and `readonly` like a base file, with parse errors reported, but no layers, no placeholders
	(`${PORT:-3000}` is just a `string`) and no `.env`. tyfigo types those files but doesn't load them at run time; Bun
	imports them natively, and elsewhere they need their own loader.
</Callout>

```yaml title="fixtures.yaml"
users:
  - name: Ada
    admin: true
```

```ts
import type fixtures from '../fixtures.yaml';
// const fixtures: { readonly users: readonly { readonly name: string; readonly admin: boolean }[] }
```

The [config-dir example](https://github.com/imranbarbhuiya/tyfigo/tree/main/examples/config-dir) runs this setup on Node.js.

## The environment name [#the-environment-name]

The environment is `NODE_ENV` by default. For browser builds and the Vite dev server, bundler plugins use the bundler's mode
instead where there is one (Vite, webpack, Rspack, Rsbuild). A server bundle reads `NODE_ENV` when it starts
([Server bundles](/docs/browser-builds#server-bundles)). Every plugin and loader takes an `environment` option, which fixes it:

```ts
tyfigo({ environment: 'staging' });
```

With no environment, only the base and `app.local.config.yaml` are read.

## Merging [#merging]

- Objects merge key by key.
- Arrays and scalar values replace what was there.
- An override may only use keys the base file has, with a value of the same kind (string, number, boolean, null, array or object).

```yaml title="config/app.config.yaml"
server:
  host: localhost
  port: ${PORT:-3000}
features: [search]
```

```yaml title="config/app.production.config.yaml"
server:
  host: 0.0.0.0
features: [search, billing]
```

With `NODE_ENV=production`, `config.server` is `{ host: '0.0.0.0', port: 8080 }` (with `PORT=8080`) and `config.features` is
`['search', 'billing']`.

A key the base file doesn't have, or a value of another kind, is a type error in tsc (see [Type checking](/docs/type-checking))
and a problem at run time:

```txt
/srv/app/config/app.production.config.yaml:3:3 - 'server.hostname' is not in app.config.yaml; environment and local files can only override existing keys (unknown-key)
```

## The imported value [#the-imported-value]

- The default export is the merged config, deep-frozen with `Object.freeze`.
- Its type is `readonly` all the way down, so assigning to it is a type error too.
- Each import resolves once, when the module is first evaluated.

Several config files can live side by side. The Vite and Rspack examples use `app.config.*` for browser code and
`server.config.*` for server code, so server-only values never reach a client bundle (see [Browser builds](/docs/browser-builds)).
