Browser builds
What ends up in a client bundle, and why placeholders fail the build.
A browser bundle is public, and it can't read .env files or environment variables when it runs. So when a config file is
imported from client code, tyfigo:
- Inlines the base and environment files only, merged at build time. Local files (
app.local.config.*) are left out. - Fails the build on any
${…}placeholder in those files, so a secret can't be inlined by accident. - Fails the build when client code imports
loadEnvor another Node.js-only export.
Invalid config /srv/app/config/bad.config.yaml
/srv/app/config/bad.config.yaml:1:6 - 'url' uses an environment placeholder, which browser builds cannot read (browser-placeholder)A literal $${ is allowed.
Which builds are browser builds
| Tool | Browser target |
|---|---|
| Vite | Client environment (not SSR) |
| Next.js | Client components and Edge code |
| webpack, Rspack, Rsbuild | A web target |
| esbuild, tsup | Any platform other than node |
| Rolldown, tsdown | Any platform other than node |
| Rollup | Always, unless you pass target: 'server' |
Bun.build | target: 'browser' |
Every plugin takes target: 'browser' | 'server' to override the detected one (except Next.js, which decides per layer).
Split client and server config
Keep values the browser may see in one file and server-only values in another. From the rspack example:
title: tyfigo + Rspack
analytics:
enabled: false
sampleRate: 1
links:
docs: http://localhost:3000/docsport: ${PORT:-3000}
cache:
url: ${CACHE_URL}
maxEntries: 100src/client.ts imports app.config.yaml, and src/server.ts imports server.config.yaml. The client bundle contains the
production values of app.config.yaml and nothing from .env.
Server bundles
Build once and deploy the same bundle to every environment. A server bundle inlines the text of every layer file that exists
when it builds: the base file, every environment file (app.<env>.config.*), and the local files (app.local.config.*,
app.<env>.local.config.*). When the server starts, it picks the layers by NODE_ENV:
config/app.config.yaml always
config/app.staging.config.yaml NODE_ENV=staging
config/app.production.config.yaml NODE_ENV=production
app.local.config.yaml alwaysSo a bundle promoted from staging to production uses app.production.config.yaml there. With no environment file for the
NODE_ENV it runs under, it uses the base and local files only. The bundler's mode and NODE_ENV at build time don't matter.
Pass environment to the plugin to fix it instead. The bundle then inlines only that environment's files and ignores
NODE_ENV when it starts:
tyfigo({ environment: 'production' });When the server starts, the bundle also resolves placeholders from the environment of the process and the .env files in its
working directory: .env, .env.local, .env.<env> and .env.<env>.local. A missing variable, or one that doesn't read as
its placeholder's type, is not a build error; it fails when the server starts. Paths of the build machine are not baked in.
Every environment file is checked when the bundle builds, so a bad app.staging.config.yaml fails a build you meant for
production. In watch mode, editing any of the inlined files rebuilds the bundle. A new environment file is picked up by the
next build. Rollup and webpack also rebuild when the file for the build's NODE_ENV is created.
Node.js-only exports
loadEnv, loadEnvAsync and layerEnv read .env files and environment variables, and configProviderFromFile and
layerConfigProviderFromFile read config files from disk. Importing any of them from client code fails the build:
/srv/app/src/client.ts:1:1 - loadEnv from 'tyfigo' reads files and environment variables with Node.js, so it cannot run in a browser bundle. Call it in server code, or set the tyfigo plugin's target option to 'server' if this bundle runs on a server.Next.js with Turbopack reports it as an export that tyfigo doesn't have. loadConfig and configProvider work in the browser. Bundlers that resolve the browser export condition get entries of
tyfigo and tyfigo/effect without the Node.js-only exports, so a client bundle carries no Node.js code.