| | |

Parcel 17 ๐Ÿ“ฆ Environment Variables โ€” .env files & process.env

โฑ๏ธ Estimated read: 13 min | |

Parcel’s environment variable handling is a build-time substitution system, not a runtime lookup. When you write process.env.API_KEY in your source, Parcel replaces that expression with the literal string value from your .env file or shell environment during the build. The result is a bundle that contains the value, not a reference to a global object. This is why the documentation insists on static property access: a dynamic lookup like process.env[key] cannot be resolved at build time, so Parcel cannot substitute it. The mechanism is simple, but the file precedence rules and the library-target behavior are where the surprises live.

The .env loading follows a cascade that depends on NODE_ENV. The base .env file is always loaded. Then, depending on the mode, Parcel loads .env.local, .env.[NODE_ENV], and .env.[NODE_ENV].local in a specific order, with more specific files overriding less specific ones. The NODE_ENV value itself is set automatically by Parcel: production during parcel build, and development otherwise. This automatic setting is what determines which environment-specific files are loaded, and it can be overridden by setting NODE_ENV in your shell before running the command.

Key point: Parcel inlines environment variables at build time. Only static property access (process.env.NAME) is supported. The value is baked into the bundle, and .env.local is deliberately skipped when NODE_ENV=test so that tests are reproducible across machines.


Why the build-time substitution model exists

The browser has no process.env. In Node.js, process.env is a live object that reflects the environment the process was started with. In a browser, there is no process global, and there is no concept of a shell environment. A bundler that targets the browser must either polyfill the process object (which adds weight and complexity) or substitute the environment variables at build time. Parcel chooses substitution. The process.env.API_KEY expression is replaced with "sk-abc123", and the resulting code runs in any browser without a polyfill.

The security boundary. The substitution is also a filtering mechanism. Only the variables that are explicitly referenced with static access are inlined. A variable in .env that is never mentioned in the source code does not appear in the output. This is not a hard security boundaryโ€”a variable that is referenced in the source is inlined regardless of whether it should be exposed to the clientโ€”but it prevents accidental leakage of variables that the build does not need. The inlineEnvironment option can be set to an array of glob patterns to further restrict which variables are allowed to be inlined, which is the recommended hardening step for applications that handle secrets.

The library-target problem. When Parcel builds a library (a package intended to be consumed by another bundler), inlining process.env.NODE_ENV at build time is often the wrong behavior. A React component library that checks process.env.NODE_ENV === 'development' expects the consumer’s bundler to perform that substitution, so that the dev warning is present in development builds and stripped in production. Parcel’s default inlining replaces the check with the library’s build-time NODE_ENV, which is production during parcel build, permanently stripping the dev warning from the library. The @parcel/transformer-js configuration with inlineEnvironment: false is intended to address this, but the behavior has been inconsistent: reports indicate that NODE_ENV may still be inlined regardless of the setting, and undefined variables may be replaced with undefined rather than left intact . The workaround for library authors is to treat the package as a Node target (engines: { node: ">=12" }), which changes Parcel’s substitution behavior to preserve the expressions .

The trade-off. Build-time substitution is fast and produces small bundles, but it means the environment is frozen at build time. A single build artifact cannot be deployed to staging and production with different API keys. Each environment requires its own build. This is the standard model for static sites and client-side applications, but it is incompatible with the twelve-factor approach of building once and configuring at runtime. A Node target preserves process.env expressions for runtime resolution, but a browser target cannot.


a. The .env file cascade and NODE_ENV

Parcel loads .env files from the project root, alongside package.json. The files are parsed with dotenv syntax: NAME=value pairs, one per line, with # for comments . The cascade depends on the NODE_ENV value.

# .env โ€” base file, always loaded
API_URL=https://api.example.com
FEATURE_FLAG=on
# .env.development โ€” loaded when NODE_ENV=development
API_URL=http://localhost:3000
# .env.production โ€” loaded when NODE_ENV=production
API_URL=https://api.example.com

The precedence order is: .env is loaded first, then .env.local (unless NODE_ENV=test), then .env.[NODE_ENV], then .env.[NODE_ENV].local. Variables in later files override variables in earlier files. The .env.local file is skipped when NODE_ENV=test because tests should produce the same result regardless of the developer’s local overrides .

The NODE_ENV value itself is set by Parcel: production for parcel build, development for parcel (the dev server). It can be overridden in the shell:

# Override NODE_ENV for a staging build
NODE_ENV=staging parcel build src/index.html

This override changes which .env.staging file is loaded, if it exists. It does not change the build modeโ€”parcel build still performs a production buildโ€”but it changes the environment variable cascade .


b. The static access requirement and the library workaround

Only static property access is supported. Parcel’s parser must be able to see the exact property name at build time.

// โœ… Supported: static property access
const apiUrl = process.env.API_URL;

// โœ… Supported: destructuring
const { API_URL, API_KEY } = process.env;

// โŒ Not supported: dynamic property access
const key = 'API_URL';
const value = process.env[key]; // Parcel cannot resolve this

The dynamic access limitation is a consequence of the substitution model. Parcel cannot know which variable to inline if the property name is determined at runtime. The expression is left as-is, which means it references a process.env object that does not exist in the browser, producing a runtime error .

For library authors who need to preserve process.env.NODE_ENV checks for the consumer’s bundler, the inlineEnvironment configuration is the documented mechanism, but the behavior has been unreliable. The workaround that reliably preserves the expressions is to declare the package as a Node target:

// package.json โ€” treat the package as a Node target
{
  "engines": {
    "node": ">=12"
  }
}

This changes Parcel’s substitution behavior. The process.env expressions are left intact in the output, and the consumer’s bundler performs the substitution. The trade-off is that the package is no longer treated as a browser target by Parcel, which may affect other optimizations, but for a library that expects to be consumed by another bundler, this is the correct behavior .


Complete Example Session

// ============================================
// PART 1: THE .ENV FILES
// ============================================

// .env
// API_URL=https://api.example.com
// ANALYTICS_KEY=abc123
// DEBUG=false

// .env.development
// API_URL=http://localhost:3000
// DEBUG=true

// .env.production
// API_URL=https://api.example.com
// DEBUG=false

// ============================================
// PART 2: STATIC ACCESS IN SOURCE
// ============================================

// src/config.js
// Only static property access is supported.
// Parcel replaces these expressions at build time.
export const apiUrl = process.env.API_URL;
export const analyticsKey = process.env.ANALYTICS_KEY;
export const isDebug = process.env.DEBUG === 'true';

// โŒ Dynamic access is not substituted.
// const key = 'API_URL';
// const url = process.env[key]; // ReferenceError at runtime

// ============================================
// PART 3: NODE_ENV AND THE CASCADE
// ============================================

// During `parcel` (dev server):
// NODE_ENV=development
// .env is loaded, then .env.development overrides
// API_URL is http://localhost:3000
// DEBUG is true

// During `parcel build`:
// NODE_ENV=production
// .env is loaded, then .env.production overrides
// API_URL is https://api.example.com
// DEBUG is false

// ============================================
// PART 4: OVERRIDING NODE_ENV IN THE SHELL
// ============================================

// NODE_ENV=staging parcel build src/index.html
//
// Parcel loads .env, then .env.staging (if it exists).
// Variables not in .env.staging fall back to .env.
// NODE_ENV itself is 'staging' in the bundle.

// ============================================
// PART 5: THE .ENV.LOCAL EXCEPTION
// ============================================

// .env.local is loaded for local overrides.
// It is NOT loaded when NODE_ENV=test.
//
// NODE_ENV=test parcel build src/index.html
// โ†’ .env.local is skipped
// โ†’ tests produce the same result on every machine

// ============================================
// PART 6: THE LIBRARY TARGET WORKAROUND
// ============================================

// package.json โ€” for a library that should preserve process.env
{
  "engines": {
    "node": ">=12"
  }
}

// With this, Parcel leaves process.env expressions intact.
// The consumer's bundler performs the substitution.
// This is the workaround for the inlineEnvironment inconsistency.

// ============================================
// PART 7: THE inlineEnvironment OPTION
// ============================================

// package.json โ€” restrict which variables are inlined
{
  "@parcel/transformer-js": {
    "inlineEnvironment": ["API_*", "ANALYTICS_*"]
  }
}

// Only variables matching the glob patterns are inlined.
// This is a hardening step for applications with secrets.
// The default is to inline all referenced variables.

// ============================================
// PART 8: THE TYPESCRIPT SETUP
// ============================================

// TypeScript does not know about process.env by default.
// Install @types/node for the process global.
// npm install --save-dev @types/node

// Do not import process from 'process'.
// Use the global process object directly.
// const key = process.env.API_KEY; // โœ…
// import process from 'process';    // โŒ

// ============================================
// PART 9: CACHE INVALIDATION
// ============================================

// Sometimes a new .env file does not take effect.
// Parcel caches the build, and the cache may not
// invalidate on .env changes in some versions.
// Delete the .cache directory and rebuild.

// rm -rf .cache
// parcel build src/index.html

// ============================================
// PART 10: VERIFICATION
// ============================================

// 1. Add a variable to .env: TEST_VAR=hello
// 2. Reference it: console.log(process.env.TEST_VAR);
// 3. Build: parcel build src/index.html
// 4. Search the output for "hello".
// 5. The value is inlined. The process.env expression is gone.
// 6. Verify the variable is not in the output if it is not referenced.

The ten parts covered the .env cascade, the static access requirement, the NODE_ENV automatic setting, the shell override, the .env.local exception, the library workaround, the inlineEnvironment option, the TypeScript setup, and the cache invalidation. The through-line is that the substitution is a build-time transformation, and every limitation follows from that fact.


Quick Reference

.env file cascade

FileLoaded whenPriority
.envAlwaysLowest
.env.localNODE_ENV !== 'test'Overrides .env
.env.[NODE_ENV]Always (if exists)Overrides local
.env.[NODE_ENV].localNODE_ENV !== 'test'Highest

NODE_ENV defaults

CommandDefault NODE_ENV
parcel (dev server)development
parcel buildproduction

Access patterns

PatternSupportedBehavior
process.env.NAMEYesInlined as string
const { NAME } = process.envYesInlined as string
process.env[key]NoLeft as-is; runtime error

Best Practices

โœ… Do This:

// Use static property access
const key = process.env.API_KEY;                                // โœ…

// Use destructuring for multiple variables
const { API_KEY, API_URL } = process.env;                       // โœ…

// Install @types/node for TypeScript
// npm install --save-dev @types/node                             // โœ…

// Delete .cache if a new .env file is not picked up
// rm -rf .cache                                                  // โœ…

โŒ Don’t Do This:

// Use dynamic property access
const key = 'API_KEY';
const value = process.env[key];                                  // โŒ

// Import process from 'process'
import process from 'process';                                   // โŒ

// Expect runtime resolution in a browser target
// The value is frozen at build time.                            // โŒ

Common Pitfalls

PitfallWhy It HappensFix
Variable is undefined in the bundleDynamic property accessUse process.env.NAME
Variable is not updated after editing .envCache not invalidatedDelete .cache and rebuild
Library’s NODE_ENV check is strippedParcel inlines at build timeSet engines: { node: ">=12" }
.env.local is not loadedNODE_ENV=testThis is intentional; use .env.test
TypeScript error on process@types/node not installedInstall the types package

Real-World Examples

1. Static access

const apiUrl = process.env.API_URL;

2. Destructuring

const { API_KEY, API_URL } = process.env;

3. NODE_ENV check

if (process.env.NODE_ENV === 'development') { console.log('dev'); }

4. .env file

API_URL=https://api.example.com

5. Shell override

NODE_ENV=staging parcel build src/index.html

6. Library target workaround

"engines": { "node": ">=12" }

7. inlineEnvironment restriction

"inlineEnvironment": ["API_*"]

8. TypeScript setup

npm install --save-dev @types/node

9. Cache invalidation

rm -rf .cache && parcel build src/index.html

10. .env.local exception

# Not loaded when NODE_ENV=test

Visual

The .env Cascade

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WHICH FILE WINS WHEN?                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  NODE_ENV=development           NODE_ENV=production          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ .env                โ”‚        โ”‚ .env                โ”‚     โ”‚
โ”‚  โ”‚ (base, lowest)      โ”‚        โ”‚ (base, lowest)      โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚             โ”‚                              โ”‚                 โ”‚
โ”‚             โ–ผ                              โ–ผ                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ .env.local          โ”‚        โ”‚ .env.local          โ”‚     โ”‚
โ”‚  โ”‚ (overrides base)    โ”‚        โ”‚ (overrides base)    โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚             โ”‚                              โ”‚                 โ”‚
โ”‚             โ–ผ                              โ–ผ                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ .env.development    โ”‚        โ”‚ .env.production     โ”‚     โ”‚
โ”‚  โ”‚ (overrides local)   โ”‚        โ”‚ (overrides local)   โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚             โ”‚                              โ”‚                 โ”‚
โ”‚             โ–ผ                              โ–ผ                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ .env.development    โ”‚        โ”‚ .env.production     โ”‚     โ”‚
โ”‚  โ”‚ .local (highest)    โ”‚        โ”‚ .local (highest)    โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                                                              โ”‚
โ”‚  .env.local is skipped when NODE_ENV=test.                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Build-Time Substitution

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WHAT HAPPENS TO process.env.API_KEY                          โ”‚
โ”‚                                                              โ”‚
โ”‚  Source code                    Bundle output                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ const key =         โ”‚        โ”‚ const key =         โ”‚     โ”‚
โ”‚  โ”‚   process.env.API_KEYโ”‚  โ”€โ”€โ”€โ”€โ–ถ โ”‚   "sk-abc123";     โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                                                              โ”‚
โ”‚  The expression is replaced with the literal value.         โ”‚
โ”‚  The process global is not needed in the browser.           โ”‚
โ”‚  The value is frozen at build time.                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Static vs Dynamic Access

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WHY DYNAMIC ACCESS FAILS                                     โ”‚
โ”‚                                                              โ”‚
โ”‚  process.env.API_KEY                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Parcel sees the     โ”‚                                    โ”‚
โ”‚  โ”‚ exact property name โ”‚                                    โ”‚
โ”‚  โ”‚ โ†’ inlines the value โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  process.env[key]                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Parcel does not     โ”‚                                    โ”‚
โ”‚  โ”‚ know the property   โ”‚                                    โ”‚
โ”‚  โ”‚ name at build time  โ”‚                                    โ”‚
โ”‚  โ”‚ โ†’ leaves it as-is   โ”‚                                    โ”‚
โ”‚  โ”‚ โ†’ runtime error     โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  The parser must see the name. Destructuring works because  โ”‚
โ”‚  the names are static.                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Loading mechanismdotenv, from project root
NODE_ENV defaultproduction (build), development (dev)
Access patternStatic only (process.env.NAME)
.env.local exceptionSkipped when NODE_ENV=test
Library workaroundengines: { node: ">=12" }
Cache invalidationDelete .cache if changes not picked up
TypeScript types@types/node

Key takeaways:

  • Environment variables are inlined at build time. The process.env.NAME expression is replaced with the literal value from the .env file or shell. The resulting bundle contains the value, not a runtime reference. This is why the browser does not need a process polyfill .
  • Only static property access is supported. Parcel must know the variable name at build time. process.env.NAME and const { NAME } = process.env work. process.env[key] does not and produces a runtime error .
  • The .env cascade depends on NODE_ENV. .env is always loaded, then .env.local, .env.[NODE_ENV], and .env.[NODE_ENV].local override it in that order. .env.local is skipped when NODE_ENV=test for reproducible tests .
  • Library targets need the Node engines workaround. Inlining process.env.NODE_ENV in a library strips the dev/prod checks that the consumer’s bundler should perform. Setting engines: { node: ">=12" } preserves the expressions .
  • The inlineEnvironment option restricts which variables are inlined. An array of glob patterns limits substitution to matching variables. This is a hardening step for applications that handle secrets .

Remember: Parcel’s environment variable handling is a build-time substitution, not a runtime lookup. The value is baked into the output, and the process.env expression is gone. This is why the browser does not need a process polyfill, and why the variable name must be static. The .env cascade determines which value is baked in, and NODE_ENV determines which files are loaded. For applications, the default behavior is correct. For libraries, the Node engines workaround preserves the expressions for the consumer’s bundler. The cache is the most common source of “it’s not working”โ€”delete it and rebuild.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!