Skip to content

@nx/vite forwards watch: false from build target options to Vite dev-server server.watch #36078

Description

@duckot13

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:

{
  "watch": false
}

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

  1. Create or use an Nx workspace with a Vite application.

  2. 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"
        }
      }
    }
  3. Ensure the referenced build target has a named configuration:

    {
      "build": {
        "executor": "@nx/vite:build",
        "configurations": {
          "development": {}
        }
      }
    }
  4. 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.

  5. Run the dev server (for the repo above run the following ):

    pnpm install
    pnpm nx serve shop
  6. Observe that watch: false from the build target options is passed into the Vite dev-server config as server.watch: false.

  7. 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

  • macOS
  • Linux
  • Windows
  • Other (Please specify)

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions