Build-time globals in Vite without the baggage

Vite’s define option gives you globally available constants without mutable global state, and lets the minifier erase code you do not ship.

Some purists say, "Never use globals." That is too dogmatic, but the warning exists for a reason.

Putting everything in the global scope creates hidden dependencies, naming collisions, and state that is difficult to test. That is why we have modules and components. Thank goodness for those.

Vite definitions look like globals, but they are not runtime global variables. Vite replaces each identifier at build time.

Definitions in Vite

Vite has a useful define option:

vite.config.ts
import { defineConfig } from 'vite'
 
export default defineConfig({
  define: {
    __NAME__: JSON.stringify('My app'),
    __DOMAIN__: JSON.stringify('example.com'),
    __IS_ANALYTICS_ENABLED__: JSON.stringify(true),
  },
})

Then declare the identifiers for TypeScript:

env.d.ts
declare const __NAME__: string
declare const __DOMAIN__: string
declare const __IS_ANALYTICS_ENABLED__: boolean

You can use any valid identifier. Wrapping names in double underscores makes collisions less likely and makes build-time definitions easy to spot.

Benefits of definitions

  • Constants are configured once and available in every transformed module, without imports or runtime lookups.
  • Literal values expose feature flags to the minifier, enabling dead-code elimination.

For example:

if (__IS_ANALYTICS_ENABLED__) {
  startAnalytics()
}

When the flag is false, a production minifier can remove the entire branch.

Usage in a large monorepo

In a real project, definitions often depend on environment variables. If several workspaces need the same values, keep a shared getDefinitions utility at the root of the monorepo.

Build configuration runs before your application bundle exists, so make this utility a dependency-free JavaScript module and type it with JSDoc:

get-definitions.mjs
/** @typedef {'production' | 'development'} NodeEnv */
/** @typedef {{ NODE_ENV: NodeEnv }} DefinedEnv */
/** @typedef {Record<string, string>} Definitions */
 
const nodeEnvs = ['development', 'production']
 
/**
 * Validates environment variables without runtime dependencies
 *
 * @param {unknown} env
 * @returns {DefinedEnv}
 */
function validate(env) {
  if (env === null || typeof env !== 'object') {
    throw new Error('DEFINITIONS_ERROR: env is invalid')
  }
 
  if (!('NODE_ENV' in env)) {
    throw new Error('DEFINITIONS_ERROR: NODE_ENV is missing')
  }
 
  const { NODE_ENV } = env
 
  if (typeof NODE_ENV !== 'string' || !nodeEnvs.includes(NODE_ENV)) {
    throw new Error('DEFINITIONS_ERROR: NODE_ENV is invalid')
  }
 
  return /** @type {DefinedEnv} */ (env)
}
 
/**
 * Creates build-time definitions from environment variables
 *
 * @param {unknown} env
 * @returns {Definitions}
 */
export function getDefinitions(env) {
  const { NODE_ENV } = validate(env)
 
  return {
    __NAME__: JSON.stringify('My app'),
    __DOMAIN__: JSON.stringify(
      {
        production: 'intternet.dev',
        development: 'localhost:5173',
      }[NODE_ENV]
    ),
    __IS_ANALYTICS_ENABLED__: JSON.stringify(
      {
        production: true,
        development: false,
      }[NODE_ENV]
    ),
  }
}

Then import it in each workspace:

apps/my-app/vite.config.ts
import { defineConfig } from 'vite'
 
import { getDefinitions } from '../../get-definitions.mjs'
 
export default defineConfig({
  define: getDefinitions(process.env),
})

Initializing definitions outside the build system

Scripts and some tests may run outside Vite. In those cases, install the same values on globalThis before running the rest of the code:

install-definitions.ts
import { getDefinitions } from './get-definitions.mjs'
 
for (const [key, value] of Object.entries(getDefinitions(process.env))) {
  Reflect.set(globalThis, key, JSON.parse(value))
}

This creates real global state, so reserve it for entry points you control.

Import the installer at the top of any file that needs the definitions:

import '../install-definitions.ts'

Definitions in Bun.build

If some of your monorepo apps build with Bun, Bun.build provides the exact same define API:

apps/my-bun-app/build.ts
import { getDefinitions } from '../../.config/get-definitions.mjs'
 
await Bun.build({
  entrypoints: ['./src/server.ts'],
  outdir: './build',
  define: getDefinitions(process.env),
})