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:
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:
declare const __NAME__: string
declare const __DOMAIN__: string
declare const __IS_ANALYTICS_ENABLED__: booleanYou 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:
/** @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:
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:
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:
import { getDefinitions } from '../../.config/get-definitions.mjs'
await Bun.build({
entrypoints: ['./src/server.ts'],
outdir: './build',
define: getDefinitions(process.env),
})