tyfigo

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:

  1. Inlines the base and environment files only, merged at build time. Local files (app.local.config.*) are left out.
  2. Fails the build on any ${…} placeholder in those files, so a secret can't be inlined by accident.
  3. Fails the build when client code imports loadEnv or 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

ToolBrowser target
ViteClient environment (not SSR)
Next.jsClient components and Edge code
webpack, Rspack, RsbuildA web target
esbuild, tsupAny platform other than node
Rolldown, tsdownAny platform other than node
RollupAlways, unless you pass target: 'server'
Bun.buildtarget: '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:

config/app.config.yaml
title: tyfigo + Rspack
analytics:
  enabled: false
  sampleRate: 1
links:
  docs: http://localhost:3000/docs
config/server.config.yaml
port: ${PORT:-3000}
cache:
  url: ${CACHE_URL}
  maxEntries: 100

src/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               always

So 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.

On this page