# 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:

```ts title="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:

```ts title="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:

```ts
if (__IS_ANALYTICS_ENABLED__) {
  startAnalytics()
}
```

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

Global definitions work best for short strings, numbers, and booleans. Do not
put large values in definitions: each reference may duplicate the value in the
bundle. Export large or structured values from a module instead.

## 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:

```js title="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:

```ts title="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:

```ts title="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:

```ts title='scripts/my-script.ts'
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:

```ts title="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),
})
```
