Config files and layering
How tyfigo names, finds and merges base, environment and local config files.
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 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
Layers apply in this order, each one over the result of the ones before it:
app.config.yamlapp.<environment>.config.yamlapp.local.config.yamlapp.<environment>.local.config.yaml- Environment variables, through 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:
*.local.config.*
.env.localA .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 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 |
import config from '../.config/app.yaml';- Environment files sit next to the base file, in the same
.configfolder. - Local files are looked for next to the base file first, then in the project root's
.configfolder. .envfiles are looked for in the folder that contains.configfirst, then in the project root, so.envsits where it usually does, not inside.config.- Files in folders below
.config(.config/db/app.yaml) are not config files.
Running it
Nothing to set up. tyfigo/register, tyfigo/bun, tyfigo/jest (with the pattern on 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
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/*:
{
"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.
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. 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.
users:
- name: Ada
admin: trueimport type fixtures from '../fixtures.yaml';
// const fixtures: { readonly users: readonly { readonly name: string; readonly admin: boolean }[] }The config-dir example runs this setup on Node.js.
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). Every plugin and loader takes an environment option, which fixes it:
tyfigo({ environment: 'staging' });With no environment, only the base and app.local.config.yaml are read.
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).
server:
host: localhost
port: ${PORT:-3000}
features: [search]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) and a problem at run time:
/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 default export is the merged config, deep-frozen with
Object.freeze. - Its type is
readonlyall 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).