Migrated the esbuild-fivem template from FiveM’s default Node 16 to Node 22, and bumped all build-time tooling to latest (esbuild 0.28, TypeScript 6, chokidar 5, chalk 5.6, @citizenfx 2.0.30906). Reusable recipe + the TS6 / chokidar-5 gotchas that actually cost time. Nothing was committed — this documents a validated-but-uncommitted change set.

For Agents

Host toolchain: Node v22.22.3, yarn 1.22.22. All updated packages are devDependencies / build-time only — the shipped dist/ is self-contained because esbuild bundles everything. Two runtimes stay independent: server = FXServer Node (now 22), client = FiveM’s own V8/CEF (chrome58, untouched). See esbuild-fivem for the two-runtime model.

Part A — Node 22 Migration (the reusable mechanism)

Four coordinated changes flip the server runtime from Node 16 to Node 22. The client is never touched.

WhereChangeWhy
fxmanifest.luaadd node_version '22'FXServer default is FiveM’s customized Node 16.x; this directive opts into the bundled Node 22 runtime. Requires up-to-date FXServer artifacts.
build.dev.js + build.prod.jsserver esbuild target: ['node22'] (was undefined)Tell esbuild it may emit Node-22-level syntax for the server bundle. Server stays format: 'cjs', platform: 'node'.
package.json@types/node^22 (resolved 22.19.20)Match runtime types to runtime. Deliberately pinned to the 22.x line — do NOT jump to 23/24, or the type surface drifts ahead of the actual FXServer runtime.
server/tsconfig.jsontarget es2016 → es2022; lib ["es2015","DOM"]["es2023","DOM"]Node 22 ships V8 12.4, which supports ES2023.

Client target stays chrome58

The client runs FiveM’s own V8/CEF runtime, completely independent of the server Node version. client esbuild target stays 'chrome58' / format 'iife'. common/tsconfig.json is intentionally NOT bumpedcommon/ is shared with the client and must remain client-compatible. Only server/tsconfig.json moves to ES2022/ES2023.

Part B — Dependency Bump + Gotchas (the valuable part)

Updated devDependencies to latest:

PackageNew version
@citizenfx/client^2.0.30906-1
@citizenfx/server^2.0.30906-1
chalk^5.6.2
chokidar^5.0.0
esbuild^0.28.0
typescript^6.0.3

Gotcha 1 — TypeScript 6 turns silent deprecations into HARD ERRORS

TS 5.3 silently accepted several options that TS 6.0.3 rejects at compile time. Three bit us:

  • moduleResolution: "node" — renamed to "node10". BUT renaming alone is not enough — TS6 still errors on the deprecated-ness. The actual fix is the "ignoreDeprecations": "6.0" flag (value "6.0" is the valid escape hatch in TS6). The nodenode10 rename is done for forward-clarity, not because it silences the error.
  • baseUrl is deprecated (TS5101) — also silenced by "ignoreDeprecations": "6.0".
  • TS6059 “file is not under rootDir” — with outDir: "./", TS6 infers rootDir as the project subdir (e.g. server/), which a cross-dir include like "../common/**/*" then violates. Fix: add "rootDir": ".." to server/ and client/ tsconfigs. common/ did NOT need it — its include is only ./**/*. Safe because every tsconfig has noEmit: true (tsc only type-checks; esbuild emits), so rootDir/outDir are functionally inert.

Net tsconfig deltas (TS6)

  • server / client / common: add "ignoreDeprecations": "6.0" + change moduleResolution to "node10".
  • server / client only: also add "rootDir": ".." (fixes TS6059 for the ../common include).

Gotcha 2 — chokidar 5 is ESM-only, but require() still works on Node 22

chokidar 5.0.0 is ESM-only ("type": "module", engines.node >= 20.19.0). build.dev.js loads it via require('chokidar').

No code change was needed. Node ≥ 20.19 supports require() of synchronous ESM (the require(esm) feature), and chokidar’s own engine floor (20.19.0) is exactly the version where that works — so the existing require('chokidar') keeps working on Node 22.

Contingency if it ever breaks ( ERR_REQUIRE_ESM)

Convert to a dynamic import inside watchFolder:

const { watch } = await import('chokidar');

This mirrors how chalk is already dynamically imported in build.dev.js.

Validation (both green, independently re-run)

  • yarn buildtsc -p server && tsc -p client && node build.prod.js exits 0, emits dist/server/server.js (CJS) + dist/client/client.js (IIFE).
  • Watch mode (node build.dev.js) starts both watchers; a live file edit triggers a clean rebuild → chokidar 5 confirmed working on Node 22.

Caveat — FiveM Node 22 is available but not bulletproof

Known stability reports (community, mid-2025)

FiveM Node 22 had community-reported instability: FXServer crashes around ~20 players, and crashes when require-ing some native modules (e.g. redis) or with certain oxmysql versions. It works but is not guaranteed rock-solid.

Fallback: revert the single manifest line node_version '22'node_version '16'. (You’d typically also drop the server esbuild target back and revert server/tsconfig.json, but the manifest line is the runtime switch.)