Vite build Unexpected token in node_modules: fix define
Why vite build fails on react-router-dom while vite dev works, which define line triggers it, and a polyfill setup that builds, reproduced on Vite 2.9.12.
TL;DR#
The vite build error comes from your define block. The package named in the message is not broken. In Vite 2, define is a plain text replacement that runs only during the production build. Object values are turned into JSON text and pasted wherever the key appears as a word, in your code and in node_modules. With process: { env: { HOME: './src' } } in define and rollup-plugin-polyfill-node in plugins, the polyfill adds an import line whose binding is called process. The define plugin then rewrites that binding into an object literal, and Rollup's parser stops at column 20. Delete global and process from define, keep only dotted constants such as 'process.env.HOME', and let a polyfill plugin provide the Node globals.
The question is the Stack Overflow question on Vite parser errors in third-party dependencies: a React + TypeScript app on Vite 2.9.12 that runs with npm run dev but fails tsc && vite build. The failure, the negative controls and the fix below were reproduced on 13 October 2026 in the official node:22-bookworm Docker image (Node 22.23.3) with the question's versions: Vite 2.9.12, react-router-dom 6.3.0 and rollup-plugin-polyfill-node 0.9.0.
The error, and why the code frame misleads#
With polyfillNode() in plugins and the question's two define entries, the build fails like this:
vite v2.9.12 building for production...
transforming...
✓ 4 modules transformed.
[commonjs] Unexpected token (1:20) in /app/node_modules/react/index.js
error during build:
SyntaxError: Unexpected token (1:20) in /app/node_modules/react/index.jsThe question's log names react-router-dom/index.js instead. With an extra CommonJS package in the app, the same plugins and define entries named that package. The message names whichever dependency the parser fails on, not a faulty one. The frame under the message prints line 1 of the original file, and that is misleading: a /** licence comment in the question, 'use strict'; for React in the reproduction. Rollup did not parse that text. It parsed the file after two other plugins had rewritten it. The stack trace gives the useful clue: parseImportSpecifiers, then parseIdent. The parser was inside import { ... } and expected a name.
A small plugin placed after polyfillNode() printed the start of a transformed dependency. Its first line was an import added by the polyfill:
import { default as process } from '\0polyfill-node.process';import { default as is 20 characters long, so 0-based column 20 is where the word process starts. That is the exact position in the error.
Why it happens: Vite 2's define is a build-time text replacement#
Three pieces of Vite 2.9.12's source explain it.
Object values become JSON text. The define plugin stringifies any value that is not already a string:
userDefine[key] = typeof val === 'string' ? val : JSON.stringify(val)So process: { env: { HOME: './src' } } turns into the text {"env":{"HOME":"./src"}}, and global: {} turns into {}. Each one is pasted, unparenthesised, wherever the key matches as a whole word.
It does nothing in dev. The same file returns early when the command is not a build. Its comment reads for dev we inject actual global defines in the vite client to avoid the transform cost. This is why npm run dev works: no module source is ever rewritten.
It runs after your plugins. In the plugin list, ...normalPlugins (your plugins array, polyfillNode() included) comes immediately before definePlugin(config). The build plugins come after that: commonjsPlugin(options.commonjsOptions), then anything in build.rollupOptions.plugins. The polyfill first adds import { default as process } ... to each dependency that uses process. Define then replaces process inside that import, which leaves import { default as {"env":{"HOME":"./src"}} } from .... The commonjs plugin parses that and fails.
The Vite 2.9 documentation warns about exactly this. From the define section of the v2.9.12 config docs:
Because it's implemented as straightforward text replacements without any syntax analysis, we recommend using
definefor CONSTANTS only.
It also says that process and global should not be put into this option, and that they can be shimmed or polyfilled instead.
The global: {} line breaks in a second way, which Vite issue #8909 describes. A Vite team member (sapphi-red) showed that if (typeof global !== 'undefined') { global.Prism = Prism; } becomes {}.Prism = Prism. At the start of a statement, { opens a block, not an object, so the line no longer parses. That is why the second answer on the question suggests global: '({})'.
What each config change does, one at a time#
Each row changes one thing from the question's configuration. Every build ran on Vite 2.9.12 in the same container.
| Change | Result |
|---|---|
None: global: {} and the process object, polyfillNode() in plugins | Unexpected token (1:20) |
Only global: {} removed | Same (1:20) error |
global set to 'globalThis', process object kept | Same (1:20) error |
Only the process object removed (global: {} kept) | Parse error @:1:24 inside the polyfill's own process shim |
polyfillNode() kept only in build.rollupOptions.plugins, define untouched | Builds, until a dependency uses process (below) |
The negative controls matter for the answers on the question. Removing or rewriting global alone does not change the (1:20) error. That error comes from the process object. The global line causes the other failures: the parse error in the process shim, and the {}.Prism statement form. In the question's configuration both lines have to go.
The last row needs a warning. The question already lists polyfillNode() in build.rollupOptions.plugins as well as in plugins. Deleting only the top-level copy lets a project build when no dependency touches process. When a CommonJS dependency calls process.nextTick(...), the process define rewrites that call into {"env":{"HOME":"./src"}}.nextTick(...), and the build fails again in that dependency:
[commonjs] Unexpected token (4:8) in /app/node_modules/fake-node-dep/index.jsSo the define entries are the cause, whatever happens to the polyfill plugin.
Fix#
1. Take global and process out of define. Keep only real constants, with the full dotted path as the key and a JSON-stringified value. The question needed process.env.HOME for a Segment analytics library, so the entry becomes:
// vite.config.js — only constants in define
export default {
define: {
'process.env.HOME': JSON.stringify('./src'),
},
};For your own code, Vite's own mechanism is import.meta.env with VITE_-prefixed variables. See why import.meta.env is undefined in a production build, which also covers code that still reads process.env.
2. Let one plugin supply the Node globals and modules. The question uses three tools for one job: @esbuild-plugins/node-globals-polyfill and @esbuild-plugins/node-modules-polyfill in optimizeDeps, rollup-plugin-polyfill-node for the build, and resolve aliases to rollup-plugin-node-polyfills. vite-plugin-node-polyfills does all of it as one Vite plugin. Its current release on the npm registry lists every Vite major from 2 to 8 as a supported peer. Install it and remove the others:
npm install --save-dev vite-plugin-node-polyfills
npm uninstall rollup-plugin-polyfill-node @esbuild-plugins/node-globals-polyfill @esbuild-plugins/node-modules-polyfillThen the whole Node part of the config becomes:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
export default defineConfig({
plugins: [react(), nodePolyfills()],
define: {
'process.env.HOME': JSON.stringify('./src'),
},
});Delete the util, events, stream, path and zlib aliases and the optimizeDeps.esbuildOptions block along with the old plugins. The question also sets build.rollupOptions.output.format to cjs. That has nothing to do with this error, but a browser app normally ships Vite's default ES module output.
Alternative to step 2: if a library only needs global and you would rather keep a define, use a string. This replaces the polyfill plugin instead of adding to it; the combination of both was not built. In the reproduction, a file with the global.Prism = Prism statement from issue #8909 built with either of these, and failed with global: {}:
export default {
define: {
global: 'globalThis',
},
};'globalThis' makes global.Prism refer to the real global object. '({})' builds too, but each occurrence of global evaluates to a new empty object, so a library that writes to global and reads the value back elsewhere will see nothing. The first answer's window.global ||= window; at the top of the entry file has the same goal, without a define.
Verify the fix#
Run the build again. To be sure the polyfills ended up in the bundle, the reproduction added a small CommonJS package under node_modules that calls require('events'), process.nextTick and Buffer.from. With the step 2 config plus build.minify set to false (to inspect the bundle), on Vite 2.9.12:
✓ 40 modules transformed.
rendering chunks...
dist/index.html 0.14 KiB
dist/assets/index.bbec4919.js 310.63 KiB / gzip: 73.40 KiBThat unminified bundle contained the events polyfill's prototype.emit implementation, the process shim's process.nextTick = function(fun), and a const Buffer = buffer.Buffer; binding next to the package's code. Vite did not swap events for its empty browser stub. Then run npm run preview and open the page: a missing polyfill shows up there as a runtime error, which the build alone cannot catch.
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 error changes after you edit the config#
Two follow-up errors appeared during the reproduction. Each one means a different half of the old setup is still there.
'createContext' is not exported by node_modules/react/index.js, imported by node_modules/react-router/index.js. This happened afterdefinewas fixed butrollup-plugin-polyfill-nodewas left in the top-levelpluginsarray, with or withouttransformMixedEsModules. React's CommonJS entry now starts with an ESimportthat the polyfill added before the commonjs plugin ran, and the build no longer finds React's named exports. Remove that plugin and use step 2.Parse error @:1:24inpolyfill-node.process.js.global: {}is still indefine, and it is rewriting the polyfill's own shim.
The reproduction's failing configuration (the question's plugins and define entries) builds without the parse error on Vite 8.3.4, which the reproduction also checked. The current define documentation says Vite now performs replacements with Oxc's define feature. Upgrading from Vite 2 is a migration of its own, though. On Vite 2, the version in the question, the documented rule is to keep process and global out of define.
Other "Unexpected token" errors in third-party code have unrelated causes. Jest hits it when it loads an ES module through its CommonJS wrapper, as in the uuid export case. Webpack has its own version of the Node-built-ins-in-the-browser problem, shown in the node:buffer UnhandledSchemeError. For this Vite error, the first thing to check is the define block: the error message points at a dependency, but the code that broke the build is in your own config.
Further reading: Fix Vite [commonjs--resolver] Failed to resolve entry. Also see Fix pnpm Ignored Build Scripts for Native Dependencies. Also see Owl Carousel not working after Vite build: reading 'fn', Vite linked React library: fix Invalid hook call.
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
Vite failed to scan dependencies: unwanted HTML entries
Reproduce a dependency scan that includes gitignored documentation, then narrow the entry list without hiding syntax errors in the application itself.
Fix: No loader is configured for ".node" files (Vite)
Trace the import that brings a native addon into the dev scan, and distinguish the tested Vite 4 behaviour from newer bundlers.
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.


