···11+---
22+title: injectCjsGlobals | Config
33+---
44+55+# injectCjsGlobals
66+77+- **Type:** `boolean`
88+- **Default:** `true`
99+- **CLI:** `--no-inject-cjs-globals`, `--injectCjsGlobals=false`
1010+1111+Inject CommonJS module variables (`module`, `exports`, `require`, `__filename`, `__dirname`) into every module processed by Vitest.
1212+1313+By default, every file that Vitest transforms has access to these variables even if it is written using ESM syntax. This doesn't reflect how modules work in the wild: browsers do not support CommonJS variables, and Node.js doesn't expose them in ES modules.
1414+1515+To make the module environment stricter and closer to the target runtime, you can disable this behaviour:
1616+1717+```js
1818+import { defineConfig } from 'vitest/config'
1919+2020+export default defineConfig({
2121+ test: {
2222+ injectCjsGlobals: false,
2323+ },
2424+})
2525+```
2626+2727+When this option is disabled, only modules that are detected to be CommonJS receive these variables. CommonJS modules always keep them because they are part of the module scope, without them the module cannot be evaluated at all. The module type is detected the same way Node.js does it:
2828+2929+1. The file extension: `.cjs` and `.cts` files are always CommonJS, `.mjs` and `.mts` files are always ES modules.
3030+2. The `type` field in the nearest `package.json`: `"module"` means ES module, `"commonjs"` means CommonJS. Same as in Node.js, the lookup stops at the first `package.json` and never crosses a `node_modules` boundary, so dependencies don't inherit the `type` of your project.
3131+3. The presence of ESM syntax in the file: if the file has no static `import`/`export` declarations and doesn't reference `import.meta`, it is treated as CommonJS. Syntax inside comments and strings doesn't affect the detection. Dynamic imports are allowed in CommonJS modules, so they don't count as ESM syntax; type-only TypeScript imports are erased during the transform, so they don't count either.
3232+3333+The syntax detection is always enabled: Vitest doesn't respect Node.js CLI flags that modify the module type resolution, like `--no-experimental-detect-module`, `--input-type` (it only applies to the string input in Node.js), or the `--experimental-default-type` flag removed in Node.js 23.
3434+3535+Referencing a CommonJS variable in an ES module throws a `ReferenceError`, just like outside of Vitest:
3636+3737+```
3838+ReferenceError: __dirname is not defined
3939+4040+"__dirname" is a CommonJS variable that is not available in ES modules, and "injectCjsGlobals" is disabled. If this module is meant to be an ES module, use "import.meta.dirname" instead of "__dirname". If it is meant to be a CommonJS module, use the ".cjs" file extension, set "type": "commonjs" in the nearest package.json, or externalize it with "server.deps.external".
4141+```
4242+4343+::: warning
4444+This option doesn't affect externalized modules which are always executed by the native runtime. Node.js provides CommonJS variables to externalized CommonJS modules on its own.
4545+4646+Note that inlined CommonJS modules are not processed by Vite plugins even when this option is enabled: `require` calls always leave the module runner, so features like mocking do not apply to them.
4747+:::
+7
docs/guide/cli-generated.md
···327327328328Inject apis globally
329329330330+### injectCjsGlobals
331331+332332+- **CLI:** `--injectCjsGlobals`
333333+- **Config:** [injectCjsGlobals](/config/injectcjsglobals)
334334+335335+Inject CommonJS variables (`module`, `exports`, `require`, `__filename`, `__dirname`) into every test module. To disable, use `--no-inject-cjs-globals` (default: `true`)
336336+330337### dom
331338332339- **CLI:** `--dom`
+43-72
packages/utils/src/resolver.ts
···11import fs from 'node:fs'
22-import { dirname, join } from 'pathe'
22+import { basename, dirname, join } from 'pathe'
3344-const packageCache = new Map<string, { type?: 'module' | 'commonjs' }>()
44+const packageScopeTypeCache = new Map<string, 'cjs' | 'esm' | 'none'>()
5566-export function findNearestPackageData(
77- basedir: string,
88-): { type?: 'module' | 'commonjs' } {
99- const originalBasedir = basedir
1010- while (basedir) {
1111- const cached = getCachedData(packageCache, basedir, originalBasedir)
66+// mirrors LOOKUP_PACKAGE_SCOPE from the ESM resolution algorithm:
77+// the lookup stops at the first package.json and never crosses
88+// the "node_modules" boundary, so typeless dependencies don't
99+// inherit the `type` field of the user's project
1010+export function lookupPackageScopeType(
1111+ directory: string,
1212+): 'cjs' | 'esm' | 'none' {
1313+ const visited: string[] = []
1414+ let result: 'cjs' | 'esm' | 'none' = 'none'
1515+ let current = directory
1616+ while (current) {
1717+ const cached = packageScopeTypeCache.get(current)
1218 if (cached) {
1313- return cached
1414- }
1515-1616- const pkgPath = join(basedir, 'package.json')
1717- if (tryStatSync(pkgPath)?.isFile()) {
1818- const pkgData = JSON.parse(stripBomTag(fs.readFileSync(pkgPath, 'utf8')))
1919-2020- if (packageCache) {
2121- setCacheData(packageCache, pkgData, basedir, originalBasedir)
2222- }
2323-2424- return pkgData
2525- }
2626-2727- const nextBasedir = dirname(basedir)
2828- if (nextBasedir === basedir) {
1919+ result = cached
2920 break
3021 }
3131- basedir = nextBasedir
2222+ if (basename(current) === 'node_modules') {
2323+ break
2424+ }
2525+ visited.push(current)
2626+ const packageJsonPath = join(current, 'package.json')
2727+ if (tryStatSync(packageJsonPath)?.isFile()) {
2828+ try {
2929+ const packageJson = JSON.parse(stripBomTag(fs.readFileSync(packageJsonPath, 'utf8')))
3030+ if (packageJson.type === 'module') {
3131+ result = 'esm'
3232+ }
3333+ else if (packageJson.type === 'commonjs') {
3434+ result = 'cjs'
3535+ }
3636+ }
3737+ catch {
3838+ // ignore malformed package.json and fall back to "none"
3939+ }
4040+ break
4141+ }
4242+4343+ const parent = dirname(current)
4444+ if (parent === current) {
4545+ break
4646+ }
4747+ current = parent
3248 }
33493434- return {}
5050+ visited.forEach(dir => packageScopeTypeCache.set(dir, result))
5151+ return result
3552}
36533754function stripBomTag(content: string): string {
···4966 }
5067 catch {
5168 // Ignore errors
5252- }
5353-}
5454-5555-export function getCachedData<T>(
5656- cache: Map<string, T>,
5757- basedir: string,
5858- originalBasedir: string,
5959-): NonNullable<T> | undefined {
6060- const pkgData = cache.get(getFnpdCacheKey(basedir))
6161- if (pkgData) {
6262- traverseBetweenDirs(originalBasedir, basedir, (dir) => {
6363- cache.set(getFnpdCacheKey(dir), pkgData)
6464- })
6565- return pkgData
6666- }
6767-}
6868-6969-export function setCacheData<T>(
7070- cache: Map<string, T>,
7171- data: T,
7272- basedir: string,
7373- originalBasedir: string,
7474-): void {
7575- cache.set(getFnpdCacheKey(basedir), data)
7676- traverseBetweenDirs(originalBasedir, basedir, (dir) => {
7777- cache.set(getFnpdCacheKey(dir), data)
7878- })
7979-}
8080-8181-function getFnpdCacheKey(basedir: string) {
8282- return `fnpd_${basedir}`
8383-}
8484-8585-/**
8686- * Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir.
8787- * @param longerDir Longer dir path, e.g. `/User/foo/bar/baz`
8888- * @param shorterDir Shorter dir path, e.g. `/User/foo`
8989- */
9090-function traverseBetweenDirs(
9191- longerDir: string,
9292- shorterDir: string,
9393- cb: (dir: string) => void,
9494-) {
9595- while (longerDir !== shorterDir) {
9696- cb(longerDir)
9797- longerDir = dirname(longerDir)
9869 }
9970}
···44import { describe, expect, onTestFinished, test } from 'vitest'
55import { createVitest } from 'vitest/node'
6677-const nodeModulesDir = join(import.meta.dirname, '../../node_modules')
77+const nodeModulesDir = join(import.meta.dirname, '../node_modules')
8899describe.each(['deprecated', 'environment'] as const)('VitestResolver with Vite SSR config in %s style', (style) => {
1010 test('merges vite ssr.resolve.noExternal with server.deps.inline', async () => {
···11+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
22+import { tmpdir } from 'node:os'
33+import { join } from 'node:path'
44+import { lookupPackageScopeType } from '@vitest/utils/resolver'
55+import { expect, it, onTestFinished } from 'vitest'
66+77+function scaffold(tree: Record<string, string>): string {
88+ const root = mkdtempSync(join(tmpdir(), 'vitest-scope-'))
99+ onTestFinished(() => rmSync(root, { recursive: true, force: true }))
1010+ for (const [relativePath, type] of Object.entries(tree)) {
1111+ const dir = join(root, relativePath)
1212+ mkdirSync(dir, { recursive: true })
1313+ writeFileSync(join(dir, 'package.json'), JSON.stringify(type ? { type } : {}))
1414+ }
1515+ return root
1616+}
1717+1818+it('resolves the "type" of the nearest package.json', () => {
1919+ const root = scaffold({ '.': 'module' })
2020+ expect(lookupPackageScopeType(join(root, 'src', 'nested'))).toBe('esm')
2121+})
2222+2323+it('reports "commonjs" scopes as cjs and typeless scopes as none', () => {
2424+ const cjsRoot = scaffold({ '.': 'commonjs' })
2525+ const typelessRoot = scaffold({ '.': '' })
2626+ expect(lookupPackageScopeType(cjsRoot)).toBe('cjs')
2727+ expect(lookupPackageScopeType(typelessRoot)).toBe('none')
2828+})
2929+3030+it('stops at the node_modules boundary and does not inherit the project type', () => {
3131+ const root = scaffold({
3232+ '.': 'module',
3333+ 'node_modules/dep-cjs': 'commonjs',
3434+ })
3535+ // a typeless dependency must not inherit the project's `type: module`
3636+ expect(lookupPackageScopeType(join(root, 'node_modules', 'dep-typeless'))).toBe('none')
3737+ // but a dependency's own package.json still wins
3838+ expect(lookupPackageScopeType(join(root, 'node_modules', 'dep-cjs'))).toBe('cjs')
3939+})
+73-4
packages/vitest/src/node/resolver.ts
···11+import type { ModuleType } from '../types/general'
12import type { ResolvedConfig, ServerDepsOptions } from './types/config'
23import { existsSync, promises as fsp } from 'node:fs'
34import { isBuiltin } from 'node:module'
45import { pathToFileURL } from 'node:url'
56import { KNOWN_ASSET_RE } from '@vitest/utils/constants'
67import { cleanUrl } from '@vitest/utils/helpers'
77-import { findNearestPackageData } from '@vitest/utils/resolver'
88+import { lookupPackageScopeType } from '@vitest/utils/resolver'
89import * as esModuleLexer from 'es-module-lexer'
910import { dirname, extname, join, resolve } from 'pathe'
1111+import {
1212+ ssrExportAllKey,
1313+ ssrImportKey,
1414+ ssrImportMetaKey,
1515+ ssrModuleExportsKey,
1616+} from 'vite/module-runner'
1017import { isWindows } from '../utils/env'
11181219export class VitestResolver {
···145152146153 id = id.replace('file:///', '')
147154148148- const package_ = findNearestPackageData(dirname(id))
149149-150150- if (package_.type === 'module') {
155155+ if (lookupPackageScopeType(dirname(id)) === 'esm') {
151156 return true
152157 }
153158···164169 catch {
165170 return false
166171 }
172172+}
173173+174174+const ESM_SYNTAX_MARKERS = [
175175+ ssrImportKey,
176176+ ssrModuleExportsKey,
177177+ ssrExportAllKey,
178178+ ssrImportMetaKey,
179179+ // TODO: use ssrExportNameKey when Vite 6 support is over
180180+ '__vite_ssr_exportName__',
181181+]
182182+183183+const CJS_GLOBALS_REFERENCE_RE = /\b(?:module|exports|require|__filename|__dirname)\b/
184184+185185+// mirrors the Node.js module detection algorithm: the file extension wins,
186186+// then the `type` field in the package scope, then the presence of
187187+// ESM syntax. the ssr transform always rewrites static imports/exports and
188188+// `import.meta` into `__vite_ssr_` helpers, so the transformed code is
189189+// checked first: it reflects the compiled output (type-only imports are
190190+// already erased), and a module without the markers cannot be an ES module.
191191+// a marker hit is then confirmed against the lexed source because the
192192+// transform preserves comments and strings that can mention the markers.
193193+// dynamic imports never count because they are allowed in CommonJS modules
194194+export async function detectModuleType(
195195+ file: string | null,
196196+ code: string,
197197+ loadSource?: () => Promise<string | null>,
198198+): Promise<ModuleType> {
199199+ if (file) {
200200+ const filepath = cleanUrl(file)
201201+ const extension = extname(filepath)
202202+ if (extension === '.cjs' || extension === '.cts') {
203203+ return 'cjs'
204204+ }
205205+ if (extension === '.mjs' || extension === '.mts') {
206206+ return 'esm'
207207+ }
208208+ const scopeType = lookupPackageScopeType(dirname(filepath))
209209+ if (scopeType !== 'none') {
210210+ return scopeType
211211+ }
212212+ }
213213+ if (!ESM_SYNTAX_MARKERS.some(marker => code.includes(marker))) {
214214+ return 'cjs'
215215+ }
216216+ // a false "esm" verdict can only break modules that reference the CommonJS
217217+ // variables, so the source is read and lexed only when both signals appear
218218+ if (!CJS_GLOBALS_REFERENCE_RE.test(code)) {
219219+ return 'esm'
220220+ }
221221+ const source = loadSource ? await loadSource() : null
222222+ if (source != null) {
223223+ try {
224224+ await esModuleLexer.init
225225+ const [, , , hasModuleSyntax] = esModuleLexer.parse(source)
226226+ if (!hasModuleSyntax) {
227227+ return 'cjs'
228228+ }
229229+ }
230230+ catch {
231231+ // the lexer cannot parse TypeScript types or non-JS sources,
232232+ // trust the markers
233233+ }
234234+ }
235235+ return 'esm'
167236}
168237169238export async function shouldExternalize(
···11import type { Span } from '@opentelemetry/api'
22-import type { DevEnvironment, EnvironmentModuleNode, FetchResult, Rollup, TransformResult } from 'vite'
33-import type { FetchFunctionOptions } from 'vite/module-runner'
44-import type { FetchCachedFileSystemResult } from '../../types/general'
22+import type { DevEnvironment, EnvironmentModuleNode, Rollup, TransformResult } from 'vite'
33+import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner'
44+import type { FetchCachedFileSystemResult, VitestFetchResult } from '../../types/general'
55import type { OTELCarrier, Traces } from '../../utils/traces'
66import type { FileSystemModuleCache } from '../cache/fsModuleCache'
77import type { VitestResolver } from '../resolver'
···1212import { join } from 'pathe'
1313import { fetchModule } from 'vite'
1414import { hash } from '../hash'
1515+import { detectModuleType } from '../resolver'
1516import { normalizeResolvedIdToUrl } from './normalizeUrl'
16171717-const saveCachePromises = new Map<string, Promise<FetchResult>>()
1818+const saveCachePromises = new Map<string, Promise<VitestFetchResult>>()
1819const readFilePromises = new Map<string, Promise<string | null>>()
19202021class ModuleFetcher {
2122 private tmpDirectories = new Set<string>()
2223 private fsCacheEnabled: boolean
2424+ // the module type is only needed by the evaluator to decide if CJS
2525+ // variables should be provided to the module, so don't waste time
2626+ // on the detection when every module receives them
2727+ private detectModuleType: boolean
23282429 constructor(
2530 private resolver: VitestResolver,
···2833 private tmpProjectDir: string,
2934 ) {
3035 this.fsCacheEnabled = config.experimental?.fsModuleCache === true
3636+ this.detectModuleType = config.injectCjsGlobals === false
3137 }
32383339 async fetch(
···233239 tmp: moduleGraphModule.transformResult.__vitestTmp,
234240 url: moduleGraphModule.url,
235241 invalidate: false,
242242+ moduleType: this.detectModuleType
243243+ ? await detectModuleType(
244244+ moduleGraphModule.file,
245245+ moduleGraphModule.transformResult.code,
246246+ this.sourceLoader(moduleGraphModule.file),
247247+ )
248248+ : undefined,
236249 }
237250 }
238251···281294 tmp: cachePath,
282295 url: cachedModule.url,
283296 invalidate: false,
297297+ moduleType: this.detectModuleType
298298+ ? await detectModuleType(cachedModule.file, cachedModule.code, this.sourceLoader(cachedModule.file))
299299+ : undefined,
284300 }
285301 }
286302···290306 importer: string | undefined,
291307 moduleGraphModule: EnvironmentModuleNode,
292308 options?: FetchFunctionOptions,
293293- ): Promise<FetchResult> {
309309+ ): Promise<VitestFetchResult> {
294310 const moduleRunnerModule = await fetchModule(
295311 environment,
296312 url,
···301317 },
302318 ).catch(handleRollupError)
303319304304- return processResultSource(environment, moduleRunnerModule)
320320+ const result: VitestFetchResult = processResultSource(environment, moduleRunnerModule)
321321+ if (this.detectModuleType && 'code' in result) {
322322+ result.moduleType = await detectModuleType(result.file, result.code, this.sourceLoader(result.file))
323323+ }
324324+ return result
325325+ }
326326+327327+ private sourceLoader(file: string | null): (() => Promise<string | null>) | undefined {
328328+ if (!file || file.startsWith('\x00') || file.startsWith('virtual:')) {
329329+ return undefined
330330+ }
331331+ return () => this.readFileConcurrently(file)
305332 }
306333307334 private async cacheResult(
···452479 return `data:application/json;base64,${Buffer.from(map).toString('base64')}`
453480}
454481455455-function getCachedResult(result: Extract<FetchResult, { code: string }>, tmp: string): FetchCachedFileSystemResult {
482482+function getCachedResult(result: Extract<VitestFetchResult, { code: string }>, tmp: string): FetchCachedFileSystemResult {
456483 return {
457484 cached: true as const,
458485 file: result.file,
···460487 tmp,
461488 url: result.url,
462489 invalidate: result.invalidate,
490490+ moduleType: result.moduleType,
463491 }
464492}
465493
+18
packages/vitest/src/node/types/config.ts
···312312 globals?: boolean
313313314314 /**
315315+ * Inject CommonJS module variables (`module`, `exports`, `require`,
316316+ * `__filename`, `__dirname`) into every module processed by Vitest.
317317+ *
318318+ * When disabled, ES modules no longer have access to CommonJS variables,
319319+ * matching how the code runs outside of Vitest. Modules detected to be
320320+ * CommonJS keep these variables because they are part of the module scope.
321321+ * The module type is detected the same way Node.js does it: the file
322322+ * extension wins, then the `type` field in the nearest package.json,
323323+ * then the presence of ESM syntax in the file.
324324+ *
325325+ * This option doesn't affect externalized modules which are always
326326+ * executed by the native runtime.
327327+ *
328328+ * @default true
329329+ */
330330+ injectCjsGlobals?: boolean
331331+332332+ /**
315333 * Running environment
316334 *
317335 * Supports 'node', 'jsdom', 'happy-dom', 'edge-runtime'