TypeScript

Cannot use 'import.meta' outside a module: Jest + Vite fix

Three fixes for import.meta.env in Jest tests of a Vite app, each reproduced, plus the secret-leak trap in the popular process.env answer.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

Jest runs test files as CommonJS unless you opt into its experimental ESM mode, so @swc/jest compiles your Vite source to CommonJS (the asker saw the same failure with esbuild-jest). import.meta only exists in ES modules, so Node rejects the compiled file with SyntaxError: Cannot use 'import.meta' outside a module. Swapping one CommonJS transformer for another does not help. To keep Jest, move every import.meta.env read into one module and point Jest at a stand-in with moduleNameMapper. The other route is to run the tests with Vitest, which reads .env the way Vite does.

The question this page answers is Stack Overflow #69613533. It involves a React and TypeScript app built by Vite, tested by Jest with @swc/jest as the transformer and testEnvironment: "jsdom". The asker wanted a fix without Babel. Every command and output below was reproduced on 19 October 2026 in a throwaway node:22-alpine container (Node 22.23.3) with Jest 29.7.0 and 30.5.2, @swc/jest 0.2.39, @swc/core 1.16.18, Vitest 5.0.5 and Vite 8.3.4.

The error, verbatim#

The smallest project that shows it has one source file that reads a Vite variable:

ts
// src/config.ts
export const apiUrl: string = import.meta.env.VITE_API_URL;

It also has one test:

ts
// src/config.test.ts
import { apiUrl } from './config';
 
test('reads the API URL', () => {
  expect(apiUrl).toBe('https://api.example.eu');
});

The Jest config is the transform line from the question, reduced to SWC:

js
// jest.config.cjs
module.exports = {
  testEnvironment: 'node',
  transform: {
    '^.+\\.(t|j)sx?$': '@swc/jest',
  },
};

Jest 29.7.0 prints the error from the question:

text
    /app29/src/config.ts:11
    const apiUrl = import.meta.env.VITE_API_URL;
                          ^^^^
 
    SyntaxError: Cannot use 'import.meta' outside a module

The reproduction used testEnvironment: 'node', which keeps the install small. The environment plays no part in this error, because the file fails to parse before any test code runs.

Why the compiled file cannot parse#

The Jest ESM documentation states the precondition: to run tests as ES modules, you must "configure your transformer to emit ESM rather than the default CommonJS (CJS)", and start Node with the VM-modules flag. Without both, every file goes through a transformer that is asked for CommonJS. Dumping what @swc/jest hands Jest for src/config.ts shows the problem:

text
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "apiUrl", {
    enumerable: true,
    get: function() {
        return apiUrl;
    }
});
const apiUrl = import.meta.env.VITE_API_URL;

The export was rewritten into exports. The import.meta line was copied through unchanged, because CommonJS has no counterpart to compile it into. Node only allows import.meta in an ES module, so the CommonJS script fails to parse. This is also why the answers on the question move the import.meta.env reads somewhere Jest never loads, or replace them with process.env. Changing compilers does not remove the line.

There is a second, quieter problem. Even where import.meta parses, import.meta.env is something Vite adds. It is not part of the language. The variants section below shows what happens when Jest is made to parse it.

Fix 1: keep Jest, read import.meta.env in one place, and map that module#

This answers the question's "without Babel" constraint. It takes two changes, and the reproduction shows that neither works alone.

  1. Move every import.meta.env read into a single module and import from it everywhere else:
ts
// src/env.ts
export const env = {
  VITE_API_URL: import.meta.env.VITE_API_URL as string,
};
ts
// src/config.ts
import { env } from './env';
 
export const apiUrl: string = env.VITE_API_URL;

On its own, this only moves the failure to the new file. The steps of this fix were run under Jest 30.5.2, so the message is the Jest 30 form described in the variants section below:

text
    Must use import to load ES Module: /app/src/env.ts
  1. Write a stand-in with the same exports and plain values. Then tell Jest to load it instead, using moduleNameMapper:
ts
// test/env.ts
export const env = {
  VITE_API_URL: 'https://api.example.eu',
};
js
// jest.config.cjs
module.exports = {
  testEnvironment: 'node',
  transform: {
    '^.+\\.(t|j)sx?$': '@swc/jest',
  },
  moduleNameMapper: {
    '^(\\.{1,2}/)+env$': '<rootDir>/test/env.ts',
  },
};

In the question's config, add the line to the existing moduleNameMapper next to the worker and CSS entries. The regex matches the relative specifiers ./env, ../env and ../../env. It does not match a package called dotenv, a file called ./environment, or a path such as ab/env. That was checked against the compiled pattern during the reproduction. The mapping applies to every file in the run, so any other module you import as ./env is replaced too. Give the env module a name that is unique in your project.

Verify the fix#

Run Jest 30.5.2 again with the mapper in place:

text
PASS src/config.test.ts
  ✓ reads the API URL (7 ms)
 
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

The production build is untouched, because only Jest consults moduleNameMapper. Vite still builds src/env.ts, where import.meta.env values are, in the words of the Vite env guide, "statically replaced at build time".

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Fix 2: run the suite with Vitest instead#

Vitest runs tests through Vite's own pipeline. Its documentation says it "exclusively autoloads environment variables prefixed with VITE_ from .env files", following Vite's own convention (features guide). The original src/config.ts, with import.meta.env read directly, passes unchanged when a .env file at the project root sets the variable:

ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';
 
export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
  },
});
text
 ✓ src/config.test.ts (1 test) 13ms
 
 Test Files  1 passed (1)
      Tests  1 passed (1)

The value really came from .env. Renaming the file and running again fails the assertion:

text
AssertionError: expected undefined to be 'https://api.example.eu' // Object.is equality

Moving a React suite involves more than this, and two points come from the Vitest docs:

  • Globals. Unlike Jest, Vitest does not provide test and expect globally by default. The Jest migration guide also warns that Testing Library's automatic DOM cleanup relies on those globals. That is why the config above sets globals: true.
  • Browser environment. For component tests, set environment: 'jsdom' and install the jsdom package, as described in the test environment guide. The reproduction above used the node environment.

For per-test values, vi.stubEnv is documented to change a variable on both process.env and import.meta.env, and vi.unstubAllEnvs restores it. The current Vitest release on the npm registry declares Node ^22.12.0, ^24.0.0 or 26 and later, so an older CI image needs upgrading first.

The accepted answer works, but check what EnvironmentPlugin('all') inlines#

The accepted answer rewrites each import.meta.env read as the matching process.env read and adds vite-plugin-environment to Vite so the build still gets the values. Both halves hold up in the reproduction, with one caveat each.

Jest does not read .env. With the process.env version of src/config.ts, Jest 29.7.0 fails until the variable is set in the shell:

text
--- no variable set
    Expected: "https://api.example.eu"
    Received: undefined
Tests:       1 failed, 1 total
--- VITE_API_URL=https://api.example.eu npx jest
PASS src/config.test.ts
Tests:       1 passed, 1 total

Set the variables in the test script, in CI, or in a file listed in Jest's setupFiles.

EnvironmentPlugin('all') has no prefix by default. In version 1.1.4 of the plugin, the default prefix is an empty string and the plugin loads env files through Vite's loadEnv. Every name your code reads through process.env therefore gets a literal value in the bundle, whatever its prefix. With a .env that holds the API URL and a database password, and a source file that reads both, the build output was:

text
  plugins: [EnvironmentPlugin("all")],
//#region src/config.ts
var e = "https://api.example.eu", t = "s3cret-db-pass";
//#endregion

Passing the prefix option, which the plugin's README documents, restricts it to the client-safe names:

text
  plugins: [EnvironmentPlugin("all", { prefix: "VITE_" })],
//#region src/config.ts
var e = "https://api.example.eu", t = process.env.DB_PASSWORD;
//#endregion

Only references in the code are replaced, so an unused secret never reaches the bundle. One stray read in front-end code is enough to inline it, though. Vite's own import.meta.env refuses this by design, as the page on import.meta.env being undefined in a production build explains with the VITE_ prefix rule.

Same root cause, other messages: Jest 30 and native ESM#

Searching for the exact string from the question can miss the newer forms of this error.

Jest 30 rewrites the message. With the same files, Jest 30.5.2 on Node 22 never shows the import.meta SyntaxError. In its runtime source, a SyntaxError on a file that contains ESM syntax is re-thrown as a module-format error:

text
    Must use import to load ES Module: /app/src/config.ts
 
    The file contains ESM syntax (import/export) that could not be executed as CommonJS. Either:

The message goes on to suggest Node 24.9 or later. Running the same project on Node 24.21.0 with Jest 30.5.2 produced the identical error, so for this case that suggestion did not help. Fix 1 and Fix 2 above both cleared it on Node 22.23.3; they were not re-run on Node 24. The same message family comes up in a plain Node setting on the page about exports is not defined in ES module scope, which covers how Node picks CommonJS or ESM per file.

Switching Jest to native ESM gets past parsing, then fails on env. Following the @swc/jest README (extensionsToTreatAsEsm for .ts and .tsx, plus Node's VM-modules flag) lets import.meta parse. Jest does not create import.meta.env, though:

text
    TypeError: Cannot read properties of undefined (reading 'VITE_API_URL')
 
    > 1 | export const apiUrl: string = import.meta.env.VITE_API_URL;
        |                                               ^

That route still needs Fix 1's mapped module, and it changes how the jest object is reached. If tests that call jest.fn() start failing after the switch, continue with ReferenceError: jest is not defined in ESM tests. If the next failure is a dependency shipping ESM, such as export in uuid's browser build, the uuid Unexpected token export fix covers mapping it to its CommonJS build.

For a Vite app whose tests do little more than read a few env variables, the mapped module is the smallest change. It keeps the question's existing Jest config and adds one moduleNameMapper line. When the suite also depends on other Vite features, such as aliases or plugins, each one has to be rebuilt in Jest. Vitest is then the move that removes the whole class of mismatch, because the tests run through the same pipeline as the app.

Further reading: Fix Babel "native ECMAScript module configuration file". Also see Fix TransformStream is not defined in Jest. Also see Fix Jest Cannot find module on absolute path imports.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote