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.
Where
Change
Why
fxmanifest.lua
add 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.js
server 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.
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 bumped — common/ 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:
Package
New 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 node → node10 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.
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.)