Fix Vite 'global is not defined': shim or move fs code
Why each define line only reveals the next error with fs-extra (reproduced on Vite 2.9.5), which shim works for Buffer-based libraries on Vite 2 and 8, and how to type it.
TL;DR#
Vite does not provide Node's global in the browser, unlike webpack 4. A library that reads it crashes at load time. There are two different situations behind the same message:
- A browser library that only reads
global(Buffer-based packages such as amazon-cognito-identity-js): putwindow.global ||= window;in its own module and import that module first in your entry file. Typing it in TypeScript needs a smalldeclare globalblock. - Code that needs the file system (
fs-extra, as in the question): no shim fixes it. Vite replacesfswith an empty stub in the browser. Run that code in Node, or let Vite list the files at build time withimport.meta.glob.
The error#
The Stack Overflow question calls readdirSync from fs-extra in a Vite vanilla-ts project, Vite ^2.9.5. Its sequence of errors reproduces exactly, here in headless Chromium against vite dev, Vite 2.9.5, fs-extra 9.1.0, reproduced on 15 October 2026:
"Uncaught ReferenceError: process is not defined", source: http://localhost:5173/node_modules/.vite/deps/fs-extra.js?v=4a110380 (91)
"Uncaught ReferenceError: global is not defined", source: http://localhost:5173/node_modules/.vite/deps/fs-extra.js?v=372e8f39 (627)
"Uncaught TypeError: Cannot read properties of undefined (reading 'substr')", source: http://localhost:5173/node_modules/.vite/deps/fs-extra.js?v=786032c9 (806)The first line comes with no config. The second appears after adding 'process.env': {} to define, the third after also adding global: {}, as the question describes. On Vite 8.3.4 the same entry file still fails on its first line, process is not defined, and a Buffer-based library still throws global is not defined in dev.
Why it happens#
global is Node's name for the global object. Browsers call it window (or globalThis). Webpack 4 used to inject a shim for it automatically, so many older packages were published assuming it exists. Vite never did. A Vite maintainer, patak-cat, answered the original report in vitejs/vite#2778:
Vite doesn't include shims for Node variables like Webpack 4 does (in Version 5, shims also needs to be added by the user).
The question's define entries make the message move without fixing anything. In Vite 2 dev, two mechanisms apply them. The dependency optimizer passes define to esbuild, so inside the pre-bundled fs-extra, process.env.NODE_DEBUG became a reference to an injected empty object. Separately, the Vite 2.9.5 client assigns each define key onto the global object, so 'process.env': {} also creates a real process object with an empty env. Every other property, such as process.cwd or process.version, is then looked up on that nearly empty object. graceful-fs, which fs-extra loads, gets past the line that read process.cwd and reaches its next Node-only line. With global: {} added it reaches this one in graceful-fs 4.2.11:
234: if (process.version.substr(0, 4) === 'v0.8') {process.version does not exist, so .substr fails. Defining process.version as well only produced the next crash, Cannot read properties of undefined (reading 'native'), on fs.realpath.native. That last one is the real reason, and it does not go away: in the browser, fs is not Node's module. Vite's troubleshooting guide says Vite does not automatically polyfill Node.js modules and that a Node module used in the browser is externalized. In the pre-bundled file, fs is a stub with none of Node's functions on it, which is why fs.realpath came back undefined. There is no directory listing for readdirSync to return, whatever goes in define.
Fix 1: a library that only reads global#
The third answer on the question hits this with an AWS Authenticator that needed the buffer package. On npm, Amplify's auth package (@aws-amplify/auth 5.6.15) depends on buffer 4.9.2 and on amazon-cognito-identity-js 6.3.14, which pins buffer 4.9.2 as well. The reproduction used amazon-cognito-identity-js 6.3.22, which also pins buffer 4.9.2 (npm registry). That buffer version's index.js reads global.TYPED_ARRAY_SUPPORT at load time. The reproduction uses amazon-cognito-identity-js directly. Importing it in Vite dev without a shim:
"Uncaught ReferenceError: global is not defined", source: http://localhost:5173/node_modules/.vite/deps/amazon-cognito-identity-js.js?v=6181381f (243)1. Create src/init.ts. The declare global block types the property, and export {} makes the file a module:
declare global {
interface Window {
global: typeof globalThis;
}
}
window.global ||= window;
export {};Without those two additions, tsc rejects the one-line version from the top-voted answer. The first error only appears because the reproduction's tsconfig sets isolatedModules; without that option you see only the second:
src/init.ts(1,1): error TS1208: 'init.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.
src/init.ts(1,8): error TS2339: Property 'global' does not exist on type 'Window & typeof globalThis'.The second error is the general TypeScript problem of adding a property to window, covered in more depth in Fix "Property does not exist on Window" in TypeScript.
2. Import it on the first line of your entry file, before any library:
import "./init";
import { CognitoUserPool } from "amazon-cognito-identity-js";
const pool = new CognitoUserPool({
UserPoolId: "eu-west-2_AbCdEfGhI",
ClientId: "1example23456789abcdefghij",
});
console.log("pool ready", pool.getUserPoolId());Why a separate file. Writing the assignment on the line above the imports in main.ts looks equivalent, and it fails with the same global is not defined. MDN's import reference explains why: import declarations are hoisted, so their side effects are produced before the rest of the module's code runs. The library has already crashed by the time the assignment would execute. A module imported first runs first.
Alternative: a define value that is an identifier. The third answer reports that global: 'window' worked for its project. The reproduction tested the globalThis form: with define: { global: "globalThis" } and no init module, the Cognito entry file loaded in dev and in a production build on Vite 2.9.5, and in a production build on Vite 8.3.4. Know what that result is worth. The Vite 2.9.5 define docs recommend define for constants only and say that process or global should not be put into this option, and that warning covers global: "globalThis" as much as any other value: it is still a text replacement, and the reproduction only shows that it worked for this library. The init module imported first avoids define altogether, which is why it is the recommended fix here. global: {} is worse for its own reason: the library gets an empty object instead of the real global object, so anything it stores on or reads from global is not there, and in a build the {} is pasted into dependency code as text, which can break parsing (see the next-error section below).
Fix 2: code that needs the file system#
If your own code calls readdirSync, readFile or anything else from fs or fs-extra, the browser is the wrong place for it. Pick where the listing should happen.
At build time, from the browser bundle. If the goal is to know which folders exist under src (pages, locales, plugins), let Vite read the disk with import.meta.glob. It matches files, not directories, so match a file each folder contains:
const pages = import.meta.glob("./pages/*/index.ts");
const folders = Object.keys(pages).map((path) => path.split("/")[2]);
console.log("folders", folders.join(","));With src/pages/home, settings and billing, each holding an index.ts, this ran in the browser with no define at all, and tsc accepted it with vite/client listed in the tsconfig types:
"folders billing,home,settings", source: http://localhost:5173/src/main.ts (3)The Vite docs show the call being transformed into an object whose keys are the matched paths and whose values are functions returning a dynamic import(). The keys are fixed when Vite transforms the file; a page module is only fetched if you call its function.
At runtime, in Node. A build script, a server endpoint or a CLI keeps fs-extra where it works. Two traps showed up while moving the question's code to an .mjs script. First, the named import from the question fails in Node because fs-extra is CommonJS:
SyntaxError: Named export 'readdirSync' not found. The requested module 'fs-extra' is a CommonJS module, which may not support all module.exports as named exports.Import the default export instead. The reverse interop problem, a TypeScript build emitting CommonJS that reads a .default property the package does not have, is covered in dayjs_1.default is not a function. Second, the question's filter passes dir.isDirectory without calling it. A function reference is always truthy, so files slip through:
import fse from "fs-extra";
const entries = fse.readdirSync("./src/pages", { withFileTypes: true });
console.log("isDirectory :", entries.filter((dir) => dir.isDirectory).map((d) => d.name));
console.log("isDirectory():", entries.filter((dir) => dir.isDirectory()).map((d) => d.name));isDirectory : [ 'README.md', 'billing', 'home', 'settings' ]
isDirectory(): [ 'billing', 'home', 'settings' ]A note on the versions in the question: fs-extra itself has never published a 9.0.13 (npm registry), while @types/fs-extra has. The reproduction installed fs-extra 9.1.0, the last 9.x release. The ^9.0.13 range is almost certainly the type package.
Verify the fix#
Open the browser console with the dev server running, then run the production build and serve it with vite preview. Check both. The two can disagree, and which one hides the bug depends on the Vite major:
- Vite 2.9.5: without any shim,
vite buildproduced a bundle that loaded fine, while dev crashed. The Vite 2.9.5 pre-bundling docs say dev converts dependencies with esbuild, while production builds use @rollup/plugin-commonjs instead. In the unminified build, the library'sglobalhad become a helper variable:
250: Buffer2.TYPED_ARRAY_SUPPORT = commonjsGlobal.TYPED_ARRAY_SUPPORT !== void 0 ? commonjsGlobal.TYPED_ARRAY_SUPPORT : typedArraySupport();- Vite 8.3.4: the production build without a shim failed in the browser too, and passed with the init module:
"Uncaught ReferenceError: global is not defined", source: http://localhost:5174/assets/index-CjsIJG8M.js (1)
"pool ready eu-west-2_AbCdEfGhI", source: http://localhost:5174/assets/index-D9oju-Dc.js (1)So a project that only ever tested its Vite 2 build may meet this error for the first time in dev, or after upgrading. In the reproduction, the init module loaded cleanly in dev on both versions and in the Vite 8.3.4 build; the identifier define did so in dev and build on Vite 2.9.5 and in the Vite 8.3.4 build. In your own project, test dev and the build both, whichever fix you pick.
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.
When the message changes#
Buffer is not definedafterglobalis fixed: the code also expects Node'sBufferglobal, a separate problem with its own Stack Overflow question. The vite-plugin-node-polyfills readme shows a globals option with a Buffer entry, next to its polyfills for Node core modules.vite buildfails withUnexpected tokeninside a dependency after you addglobal: {}todefine: the empty object is pasted into the library's source as text. Continue with Vite build Unexpected token in node_modules, which takes that build error apart.- A similar
ReferenceErrorin tests: ReferenceError: jest is not defined is the case where Jest runs under native ES modules and thejestobject has to be imported explicitly from @jest/globals.
The question's chain of errors is worth reading as a signal rather than a to-do list. When each define entry produces a new crash deeper inside the same package, the package is walking through code written for Node, and the browser will never satisfy it. Shim global for libraries that only need the name; move file-system code to Node or to import.meta.glob.
Further reading: Fix MikroORM: global EntityManager instance is disallowed. Also see Fix Vite: crypto.hash is not a function. Also see Owl Carousel not working after Vite build: reading 'fn'.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Vite: crypto.hash is not a function
Check the Node binary that actually starts Vite, distinguish API availability from the supported version range, and verify the fix without replacing the lockfile.
Fix Vite 'Top-level await is not available' build error
Why vite dev works but vite build fails on a top-level await, which setting fixes the build and which one only fixes dev, reproduced on Vite 2.9, 5.4, 7.3 and 8.3.
Fix MikroORM: global EntityManager instance is disallowed
A fresh MikroORM 5/6 project throws a ValidationError on the first em.create() call. Here is why the global EntityManager is blocked, and the two ways to fix it properly.


