Recursos TypeScript do FiveM: fxmanifest, tipos @citizenfx e esbuild
Escreva recursos FiveM em TypeScript ou JavaScript: fxmanifest com arquivos dist, tipos @citizenfx, um bundle esbuild, eventos, setTick e Wait, e chamando exports Lua.
Você quer tipos, async/await e pacotes npm em seu script FiveM, mas seu recurso é apenas arquivos .lua. FiveM executa JavaScript em seu próprio tempo de execução, portanto TypeScript funciona desde que seja compilado para JavaScript primeiro. Este guia fornece o layout funcionando: manifest, tipos, bundler, eventos e ticks, e como misturá-lo com Lua.
Layout do projeto
my_resource/
fxmanifest.lua
package.json
tsconfig.json
src/
client.ts
server.ts
dist/ (build output, loaded by FiveM)Você edita src/, constrói em dist/, e o manifest só conhece sobre dist/.
O fxmanifest
fx_version 'cerulean'
game 'gta5'
client_script 'dist/client.js'
server_script 'dist/server.js'Ambos são arquivos JavaScript simples. Se seu recurso precisar de uma versão particular do runtime JavaScript ou funcionalidades Node no servidor, verifique a documentação atual do Cfx.re para as opções do manifest, pois os tempos de execução suportados mudaram ao longo do tempo.
Dica: se você deixar de fora a construção, FiveM diz que não consegue encontrar
dist/client.js. Sempre execute a construção antes deensureourestart.
Instale tipos e um bundler
npm init -y
npm install --save-dev typescript esbuild @citizenfx/client @citizenfx/server@citizenfx/client e @citizenfx/server são os pacotes de tipo para os natives e funções Cfx.re em cada lado. Um tsconfig.json que os usa:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}TypeScript apenas verifica tipos aqui (noEmit). esbuild faz o bundling real:
{
"scripts": {
"build": "esbuild src/client.ts --bundle --outfile=dist/client.js --target=es2020 && esbuild src/server.ts --bundle --outfile=dist/server.js --target=es2020 --platform=node",
"watch": "npm run build -- --watch",
"typecheck": "tsc"
}
}Bundling significa pacotes npm que você importa são copiados para o único arquivo de saída, portanto o servidor não precisa de node_modules em tempo de execução. Verifique a documentação Cfx.re para a --platform e --target corretas para o tempo de execução que seus artefatos usam.
Eventos
JavaScript tem os mesmos quatro blocos de construção que Lua, com outros nomes:
| Lua | JavaScript |
|---|---|
AddEventHandler |
on |
RegisterNetEvent + handler |
onNet |
TriggerEvent |
emit |
TriggerServerEvent / TriggerClientEvent |
emitNet |
// src/server.ts
onNet('my_resource:requestData', () => {
const src = (global as any).source as number
emitNet('my_resource:data', src, { hello: 'world' })
})// src/client.ts
on('onClientResourceStart', (resourceName: string) => {
if (GetCurrentResourceName() !== resourceName) return
emitNet('my_resource:requestData')
})
onNet('my_resource:data', (data: { hello: string }) => {
console.log(data.hello)
})No servidor, o jogador que disparou o evento está no source global, como em Lua. Leia-o em uma constante no início do handler: após um await, o global pode apontar para outro jogador. As mesmas regras de segurança de eventos do servidor de segurança se aplicam, já que o jogador pode disparar seu onNet de fora. Mais sobre o modelo de evento em eventos de cliente e servidor.
Ticks e espera
Não há loop de quadro por padrão. setTick executa uma função a cada quadro, e Wait do Lua se torna uma promise em torno de setTimeout:
const Delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
setTick(async () => {
const ped = PlayerPedId()
const [x, y, z] = GetEntityCoords(ped, true)
// ... check something about the position
await Delay(500) // without this, it runs every frame
})Sem um atraso, o handler é executado a cada quadro, o mesmo problema que um loop Lua sem Wait(0): veja Wait e threads explicadas. Use um atraso mais longo quando nada precisar ser verificado a cada quadro.
Para um atraso único, pule setTick e apenas await Delay(1000) dentro de uma função async.
Misturando com exports Lua
Exports funcionam entre linguagens. Registre um em JavaScript:
exports('getGreeting', (name: string) => `Hello ${name}`)E chame-o de um recurso Lua:
local text = exports['my_resource']:getGreeting('Mic')A outra direção, chamando um export Lua de TypeScript, passa pelo objeto exports:
const ok = exports['ox_inventory'].GetItemCount(source, 'bread')Declare a forma você mesmo se os tipos não conhecerem o outro recurso, por exemplo com declare const exports: Record<string, any>. Veja exports em Lua para como eles se comportam quando o recurso de destino reinicia.
Construa antes de começar
Execute o observador enquanto você trabalha, e reinicie o recurso no console após cada construção:
npm run watch
restart my_resourceSe um reinício não fizer nada novo, verifique se o arquivo de saída mudou, se o manifest aponta para ele, e se o recurso não é servido a partir de uma cópia antiga em cache/.
Checklist
| Sintoma | Correção |
|---|---|
FiveM não consegue encontrar dist/client.js |
Execute a construção; o manifest carrega a saída, não src/ |
| Natives aparecem como desconhecidos no editor | Instale @citizenfx/client e @citizenfx/server |
import falha em tempo de execução |
Bundle com esbuild para que as importações sejam incorporadas |
| Handler usa o jogador errado | Leia source em uma constante antes de qualquer await |
| Alto resmon de um tick | Adicione await Delay(ms) ao handler setTick |
| Lua não consegue ver sua função | Registre-a com exports('name', fn) e chame-a pelo nome do recurso |
Respostas rápidas
Posso usar TypeScript em um recurso FiveM diretamente?
Não. FiveM executa JavaScript. Você escreve TypeScript, o agrupa em JavaScript simples com uma ferramenta como esbuild, e aponta client_script e server_script para os arquivos de saída.
Um recurso TypeScript pode chamar exports Lua?
Sim. Exports são compartilhados entre linguagens: chame exports['my_resource'].functionName() do JavaScript, e Lua pode chamar exports que você registra com exports('name', fn) em JavaScript.
O que substitui Wait() em JavaScript?
Use uma promise em torno de setTimeout dentro de uma função async ou um handler setTick. O loop de ticking em si é setTick, e cada tick deve esperar um atraso para que não seja executado a cada quadro.
Scripts sem esse problema
Quest CreatorUm editor visual de missões e diálogos com NPCs, montado nó por nó dentro do jogo.Ver script →
Item Creator V2Crie itens usáveis com animações, props, efeitos e muito mais — sem escrever código.Ver script →
Shop CreatorMonte uma loja em menos de um minuto — donos, funcionários, cofres e assaltos inclusos.Ver script →