Current Behavior
When using @nx/vite with a Vite dev-server target that references a build target with a named configuration, Nx can forward the build target’s watch: false option into the Vite dev-server config as server.watch: false.
This appears to happen because the Vite dev-server executor merges the configured build target options into the dev-server options:
watch: buildOptionsFromBuildTarget?.watch ?? serverOptions?.watch
When the build target has a named configuration, the build target options can include the schema default:
That means the final Vite dev-server config can contain:
{
server: {
watch: false
}
}
This causes downstream Vite plugins to receive an unexpected server.watch value. Vite’s dev-server server.watch option expects a watch options object or null, whereas false is a boolean value from the Nx build executor option.
This can break plugin code that is otherwise safe when server.watch is unset.
For example, this is the kind of code that fails when config.server.watch is false:
function ignoreFederationGeneratedFiles(config, options) {
config.server ??= {};
config.server.watch ??= {};
const federationIgnore = (file) => shouldIgnoreFile(file, options);
const ignored = config.server.watch.ignored;
if (!ignored) {
config.server.watch.ignored = federationIgnore;
return;
}
if (Array.isArray(ignored)) {
ignored.push(federationIgnore);
return;
}
config.server.watch.ignored = [ignored, federationIgnore];
}
The important part is:
config.server.watch ??= {};
This works correctly when config.server.watch is undefined or null, because it initialises watch to an object.
However, when config.server.watch is false, the nullish assignment does not replace it. The code then attempts to assign:
config.server.watch.ignored = federationIgnore;
In ESM / strict mode this throws because properties cannot be assigned to the boolean value false.
This issue was surfaced after upgrading to @module-federation/vite >= 1.16.5, but the underlying issue appears to be that Nx is forwarding a build executor boolean option into Vite dev-server server.watch. @module-federation/vite had switched to ESM.
Expected Behavior
Nx should not pass watch: false into Vite dev-server server.watch.
If the build target’s watch option is false, Nx should treat it as unset when constructing the Vite dev-server config.
Expected result:
buildOptionsFromBuildTarget.watch === false
should produce either:
server.watch === undefined
or no server.watch property being passed to Vite.
Valid Vite watch option objects should continue to be preserved.
It is unclear whether the original intent of watch: false was to map to Vite’s server.watch: null behaviour.
However, for a patch-level fix, normalising false to undefined appears to be the safest option because it introduces the least behavioural change. The current implementation does not produce null behaviour today; instead, it forwards the literal boolean value false into the Vite dev-server configuration.
Treating false as undefined would preserve Vite’s default dev-server watch behaviour, avoid breaking existing consumers, and prevent invalid boolean values from being passed through to Vite plugins.
GitHub Repo
https://github.com/duckot13/vite-config-bug
Steps to Reproduce
-
Create or use an Nx workspace with a Vite application.
-
Configure the Vite dev-server executor to use a build target with a named configuration:
{
"serve": {
"executor": "@nx/vite:dev-server",
"options": {
"buildTarget": "my-app:build:development"
}
}
}
-
Ensure the referenced build target has a named configuration:
{
"build": {
"executor": "@nx/vite:build",
"configurations": {
"development": {}
}
}
}
-
Add a Vite plugin that expects config.server.watch to be unset, null, or an object. One example is @module-federation/vite >= 1.16.5.
-
Run the dev server (for the repo above run the following ):
pnpm install
pnpm nx serve shop
-
Observe that watch: false from the build target options is passed into the Vite dev-server config as server.watch: false.
-
The downstream Vite plugin can then fail when it attempts to use config.server.watch as an object.
Nx Report
Node : 22.22.3
OS : darwin-arm64
Native Target : aarch64-macos
pnpm : 11.0.5
daemon : Available
nx : 23.0.0
@nx/js : 23.0.0
@nx/eslint : 23.0.0
@nx/workspace : 23.0.0
@nx/jest : 23.0.0
@nx/devkit : 23.0.0
@nx/esbuild : 23.0.0
@nx/eslint-plugin : 23.0.0
@nx/module-federation : 23.0.0
@nx/node : 23.0.0
@nx/playwright : 23.0.0
@nx/react : 23.0.0
@nx/rollup : 23.0.0
@nx/vite : 23.0.0
@nx/vitest : 23.0.0
@nx/web : 23.0.0
@nx/docker : 23.0.0
typescript : 5.9.3
---------------------------------------
Registered Plugins:
@nx/js/typescript
@nx/react/router-plugin
@nx/eslint/plugin
@nx/vite/plugin
@nx/playwright/plugin
@nx/js/typescript
@nx/vitest
---------------------------------------
Cache Usage: 102.60 KB / 46.04 GB
Failure Logs
> nx run @org/shop:serve
The `@nx/vite:dev-server` executor is deprecated and will be removed in Nx v24. Run `nx g @nx/vite:convert-to-inferred` to migrate to the `@nx/vite/plugin` inferred targets. See https://nx.dev/docs/guides/tasks--caching/convert-to-inferred for details.
TypeError: Cannot create property 'ignored' on boolean 'false'
at ignoreFederationGeneratedFiles (file:///Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/@module-federation+vite@1.16.9_typescript@5.9.3_vite@8.0.16_@types+node@20.19.9_esbuild_45b6a1fa7e1ddc26e7c00d0df1caca06/node_modules/@module-federation/vite/lib/index.js:5129:31)
at BasicMinimalPluginContext.config (file:///Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/@module-federation+vite@1.16.9_typescript@5.9.3_vite@8.0.16_@types+node@20.19.9_esbuild_45b6a1fa7e1ddc26e7c00d0df1caca06/node_modules/@module-federation/vite/lib/index.js:5207:30)
at runConfigHook (file:///Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/vite@8.0.16_@types+node@20.19.9_esbuild@0.27.7_jiti@2.4.2_terser@5.48.0_yaml@2.9.0/node_modules/vite/dist/node/chunks/node.js:35033:42)
at resolveConfig (file:///Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/vite@8.0.16_@types+node@20.19.9_esbuild@0.27.7_jiti@2.4.2_terser@5.48.0_yaml@2.9.0/node_modules/vite/dist/node/chunks/node.js:34490:17)
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
at async _createServer (file:///Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/vite@8.0.16_@types+node@20.19.9_esbuild@0.27.7_jiti@2.4.2_terser@5.48.0_yaml@2.9.0/node_modules/vite/dist/node/chunks/node.js:26197:65)
at async viteDevServerExecutor (/Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/@nx+vite@23.0.0_af8489c623c0bf8201b592d74ed10533/node_modules/@nx/vite/dist/src/executors/dev-server/dev-server.impl.js:47:24)
at async getLastValueFromAsyncIterableIterator (/Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/nx@23.0.0_@swc-node+register@1.11.1_@swc+core@1.15.8_@swc+helpers@0.5.18__@swc+types@0._fa9d1dcff89cc312bd8cd7192aa2c27c/node_modules/nx/dist/src/utils/async-iterator.js:15:19)
at async iteratorToProcessStatusCode (/Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/nx@23.0.0_@swc-node+register@1.11.1_@swc+core@1.15.8_@swc+helpers@0.5.18__@swc+types@0._fa9d1dcff89cc312bd8cd7192aa2c27c/node_modules/nx/dist/src/command-line/run/run.js:40:25)
at async handleErrors (/Users/tom.duckworth/Desktop/Development/org/node_modules/.pnpm/nx@23.0.0_@swc-node+register@1.11.1_@swc+core@1.15.8_@swc+helpers@0.5.18__@swc+types@0._fa9d1dcff89cc312bd8cd7192aa2c27c/node_modules/nx/dist/src/utils/handle-errors.js:9:24)
Package Manager Version
pnpm@11.0.5
Operating System
Additional Information
The immediate patch-level fix could be to normalise false to undefined when constructing Vite dev-server server.watch.
This keeps the current precedence behavior, but prevents false from leaking into Vite’s dev-server config.
A broader fix may be to update the relevant @nx/vite target option schema so that the watch option better reflects the Vite server.watch type.
Currently, the schema allows a boolean and defaults to false:
{
"watch": {
"description": "Enable re-building when files change.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "object"
}
],
"default": false
}
}
However, for Vite dev-server server.watch, the expected shape is closer to:
{
"watch": {
"description": "Configure Vite dev-server file watching.",
"oneOf": [
{
"type": "null"
},
{
"type": "object"
}
]
}
}
In that case, there would be no default, so the value remains undefined unless explicitly configured.
That said, changing the schema may be a broader or breaking change because watch: false is currently valid for Nx build executor options. For a patch fix, normalising false to undefined before passing it into Vite dev-server options may be the safest option.
Patch PR: #36080
Current Behavior
When using
@nx/vitewith a Vite dev-server target that references a build target with a named configuration, Nx can forward the build target’swatch: falseoption into the Vite dev-server config asserver.watch: false.This appears to happen because the Vite dev-server executor merges the configured build target options into the dev-server options:
When the build target has a named configuration, the build target options can include the schema default:
{ "watch": false }That means the final Vite dev-server config can contain:
This causes downstream Vite plugins to receive an unexpected
server.watchvalue. Vite’s dev-serverserver.watchoption expects a watch options object ornull, whereasfalseis a boolean value from the Nx build executor option.This can break plugin code that is otherwise safe when
server.watchis unset.For example, this is the kind of code that fails when
config.server.watchisfalse:The important part is:
This works correctly when
config.server.watchisundefinedornull, because it initialiseswatchto an object.However, when
config.server.watchisfalse, the nullish assignment does not replace it. The code then attempts to assign:In ESM / strict mode this throws because properties cannot be assigned to the boolean value
false.This issue was surfaced after upgrading to
@module-federation/vite >= 1.16.5, but the underlying issue appears to be that Nx is forwarding a build executor boolean option into Vite dev-serverserver.watch. @module-federation/vite had switched to ESM.Expected Behavior
Nx should not pass
watch: falseinto Vite dev-serverserver.watch.If the build target’s
watchoption isfalse, Nx should treat it as unset when constructing the Vite dev-server config.Expected result:
should produce either:
or no
server.watchproperty being passed to Vite.Valid Vite watch option objects should continue to be preserved.
It is unclear whether the original intent of
watch: falsewas to map to Vite’sserver.watch: nullbehaviour.However, for a patch-level fix, normalising
falsetoundefinedappears to be the safest option because it introduces the least behavioural change. The current implementation does not producenullbehaviour today; instead, it forwards the literal boolean valuefalseinto the Vite dev-server configuration.Treating
falseasundefinedwould preserve Vite’s default dev-server watch behaviour, avoid breaking existing consumers, and prevent invalid boolean values from being passed through to Vite plugins.GitHub Repo
https://github.com/duckot13/vite-config-bug
Steps to Reproduce
Create or use an Nx workspace with a Vite application.
Configure the Vite dev-server executor to use a build target with a named configuration:
{ "serve": { "executor": "@nx/vite:dev-server", "options": { "buildTarget": "my-app:build:development" } } }Ensure the referenced build target has a named configuration:
{ "build": { "executor": "@nx/vite:build", "configurations": { "development": {} } } }Add a Vite plugin that expects
config.server.watchto be unset,null, or an object. One example is@module-federation/vite >= 1.16.5.Run the dev server (for the repo above run the following ):
Observe that
watch: falsefrom the build target options is passed into the Vite dev-server config asserver.watch: false.The downstream Vite plugin can then fail when it attempts to use
config.server.watchas an object.Nx Report
Failure Logs
Package Manager Version
pnpm@11.0.5
Operating System
Additional Information
The immediate patch-level fix could be to normalise
falsetoundefinedwhen constructing Vite dev-serverserver.watch.This keeps the current precedence behavior, but prevents
falsefrom leaking into Vite’s dev-server config.A broader fix may be to update the relevant
@nx/vitetarget option schema so that thewatchoption better reflects the Viteserver.watchtype.Currently, the schema allows a boolean and defaults to
false:{ "watch": { "description": "Enable re-building when files change.", "oneOf": [ { "type": "boolean" }, { "type": "object" } ], "default": false } }However, for Vite dev-server
server.watch, the expected shape is closer to:{ "watch": { "description": "Configure Vite dev-server file watching.", "oneOf": [ { "type": "null" }, { "type": "object" } ] } }In that case, there would be no default, so the value remains
undefinedunless explicitly configured.That said, changing the schema may be a broader or breaking change because
watch: falseis currently valid for Nx build executor options. For a patch fix, normalisingfalsetoundefinedbefore passing it into Vite dev-server options may be the safest option.Patch PR: #36080