create-lr-resource

Create a production-ready typed FiveM resource from the LR v2 template


Keywords
fivem, lua, nui, mobx, typescript, scaffold
License
MIT
Install
npm install create-lr-resource@0.4.0

Documentation

LR Boilerplate MobX v2

Boilerplate FiveM/GTA5 theo kiến trúc module-first, side-isolated và decorated-store-first. Mỗi module có thể có phần shared, client, server và NUI riêng, nhưng LuaLS chỉ nạp đúng API của side đang làm việc. Client Lua, server Lua và NUI TypeScript dùng chung contract TypeBox sinh từ NUI store, đồng thời mọi payload đều được validate ở runtime.

V2 là breaking release. Không có compatibility shim cho Main, Impl, ImplCall, dynamic method dispatch hay hot reload source Lua của v1.

Yêu cầu

  • FiveM/GTA5.
  • Node.js 24.
  • pnpm 10.32.1.
  • VS Code với extension Lua Language Server (Sumneko/LuaLS).
  • LuaLS CLI 3.19.1 nếu chạy kiểm tra Lua cục bộ.

Core chạy standalone, không phụ thuộc ESX, QB, ProjectStarboy, ox_lib hay oxmysql. Optional bridge cho ESX Legacy, QBCore và Qbox được đặt ngoài core và không tạo hard dependency trong manifest.

Bắt đầu

Tạo một resource mới từ npm package:

npx create-lr-resource@latest --name my_inventory
cd my_inventory

Các cú pháp tương đương:

npm create lr-resource@latest -- --name my_inventory
pnpm dlx create-lr-resource@latest --name my_inventory

CLI từ chối tên không an toàn và thư mục đích có dữ liệu, tự đổi manifest, package scripts và tên .code-workspace, đồng thời không copy .git, cache, dependency hoặc build artifacts. Trong checkout của boilerplate có thể dùng pnpm create:resource -- --name my_inventory để chạy cùng generator cục bộ.

Từ thư mục resource:

pnpm install --frozen-lockfile
pnpm generate
pnpm verify

Phát triển NUI:

pnpm --dir web dev
pnpm --dir web start:game

start:game build NUI ở watch mode. Runtime Lua không hot reload module; hãy restart resource sau khi thay đổi Lua hoặc contract.

Cấu trúc

client/                 client runtime, generated schemas, client modules
server/                 server runtime, generated schemas, server modules
shared/                 pure shared runtime, generated DTOs, shared helpers
packages/contracts/     neutral contract authoring, TypeBox DSL and generated contracts
packages/nui-store/     standard Store/State decorators and metadata extraction
tooling/codegen/        deterministic TypeScript/Lua/LuaLS generator
tooling/cfx/            pinned GTA5/CitizenFX metadata snapshot and type generator
types/                   Cfx and generated declarations split by side
web/src/nui/            typed NUI runtime
web/src/app/modules/    decorated MobX stores and explicit registry
web/src/app/            MobX composition and application runtime
docs/                   architecture, security, authoring, migration, release
tests/                  Lua, TypeScript, security and packaging tests
packages/create-lr-resource/ npm initializer and versioned template snapshot

Client/server framework adapters nằm trong client/bridges/server/bridges/.

Mở lr_boilerplate_mobx.code-workspace thay vì mở một folder đơn. Workspace tách lua-client, lua-server, lua-sharednui, do đó client không autocomplete server API và ngược lại.

Module lifecycle

Module được đăng ký độc lập ở mỗi side:

LR.Modules:Register({
    id = "inventory",
    dependencies = { "player" },
    start = "player",

    ---@param context LRClient.ModuleContext
    create = function(context)
        return {
            Start = function(self)
                context.logger:Info("inventory started")
            end,
            Stop = function(self, stopContext)
                stopContext.logger:Info("inventory stopped")
            end,
        }
    end,
})

shared/modules/<id>.lua, client/modules/<id>.luaserver/modules/<id>.lua có thể cùng đăng ký một ID. Runtime giữ catalog riêng, sau đó compose shared part trước side part khi start và dừng theo thứ tự đảo ngược.

Lifecycle theo phase là Create all -> OnReady all -> Start all -> Stop reverse. OnReady chuẩn bị dữ liệu/API riêng; Start chạy sau khi mọi module cùng phase đã sẵn sàng nên có thể gọi module khác. Dependency được topological sort; dependency thiếu, cycle hoặc resource module phụ thuộc player module sẽ làm boot fail rõ ràng. Khi resource stop, module và disposable được dọn theo thứ tự ngược.

Chỉ cấu hình enablement trong config.lua:

Config.Modules = {
    example = true,
    inventory = true,
}

Framework bridge

Chọn framework rõ ràng trong config.lua; mặc định vẫn standalone:

Config.Framework = {
  Name = "qbx", -- standalone | auto | esx | qbcore | qbx
}

Standard boot inject bridge typed vào context.services.framework. Client có GetPlayerData, IsPlayerLoaded, Notify và subscriptions load/unload/data-change; server có GetPlayer(source), IsPlayerLoaded(source), Notify và cùng lifecycle subscriptions. Bridge chỉ trả DTO trung lập, không trả raw core/player object.

auto chọn standalone khi không thấy framework, chọn khi có đúng một framework và fail nếu nhiều framework cùng chạy. Với explicit selection, resource tương ứng phải start trước resource này. Xem Framework bridges.

Domain OOP

LR.Class cung cấp prototype inheritance cho domain entities mà không copy methods hoặc default tables vào từng instance:

local Entity = LR.Class:Define("Entity")
local Tree = Entity:Extend("Tree")

function Entity:init(input)
  self.id = input.id
end

function Tree:init(input)
  Entity.init(self, input)
  self.water = input.water
end

Tree:Property("water", {
  validate = function(_, value)
    return type(value) == "number", "water must be a number"
  end,
  set = function(_, value)
    return math.max(0, math.min(100, value))
  end,
})

local tree = Tree:New({id = "tree_1", water = 50})

Class objects dùng cho domain model, không thay thế module descriptor. Chuyển instance thành DTO table thuần trước khi gửi qua NUI, network hoặc exports. Xem API và pattern LuaLS đầy đủ trong usage guide. Ví dụ nông trại có comment và có thể chạy được được tách theo side tại server/classes/farming/, server/services/farming_service.lua, client/classes/farming/client/services/farming_service.lua.

Contract và generated API

TypeBox schemas và boundary definitions trung lập nằm tại packages/contracts/src/authoring/<module_id>/<module_id>.schemas.ts<module_id>.boundaries.ts; decorated MobX store vẫn nằm tại web/src/app/modules/<module_id>/<module_id>.store.ts:

// packages/contracts/src/authoring/inventory/inventory.boundaries.ts
import type {ModuleBoundaries} from '../../index';

export const inventoryNuiBoundary = {
  events: {toNui: {}, toClient: {}},
  rpc: {toNui: {}, toClient: {}},
} as const satisfies ModuleBoundaries['nui'];

export const inventoryNetworkBoundary = {
  events: {clientToServer: {}, serverToClient: {}},
  rpc: {clientToServer: {}, serverToClient: {}},
} as const satisfies ModuleBoundaries['net'];

export const inventoryExportBoundary = {
  client: {},
  server: {},
} as const satisfies ModuleBoundaries['exports'];

export const inventoryBoundaries = {
  nui: inventoryNuiBoundary,
  net: inventoryNetworkBoundary,
  exports: inventoryExportBoundary,
} as const satisfies ModuleBoundaries;

// packages/contracts/src/authoring/inventory/index.ts
export * from './inventory.schemas.ts';

// packages/contracts/src/authoring/index.ts
import {inventoryBoundaries} from './inventory/inventory.boundaries';

export const moduleBoundaries = Object.freeze({
  inventory: inventoryBoundaries,
});

// web/src/app/modules/inventory/inventory.store.ts
import {State, Store} from '@lr/nui-store';
import type {ContractStatic} from '@lr/contracts';
import {InventorySnapshotSchema} from '@lr/contracts/authoring/inventory';

@Store({id: 'inventory'})
export class InventoryStore {
  @State(InventorySnapshotSchema, {items: []})
  accessor snapshot!: ContractStatic<typeof InventorySnapshotSchema>;
}

nui, netexports là ba sibling module boundaries. tooling/codegen compose @State metadata từ storeModules với moduleBoundaries và bắt buộc hai registry có one-to-one parity. Browser generated contracts chỉ chứa synchronized state và NUI; net/exports chỉ nằm trong full package/Lua/LuaLS outputs. Web store chỉ import state schemas qua public module subpath như @lr/contracts/authoring/inventory. Per-module authoring/<module_id>/index.ts chỉ export schemas. Boundary files và root authoring index không được package-export cho browser, nhờ đó net/exports không lọt vào browser bundle.

Thêm store vào explicit registry web/src/app/modules/registry.ts, rồi generate:

pnpm generate:contracts
pnpm check:contracts

packages/contracts/src/modules/ và mọi thư mục generated/ là output. Generated files được commit nhưng không chỉnh tay; CI generate lại và fail nếu output drift.

Client Lua truy cập NUI qua accessor cụ thể:

local Nui = LR.Nui.Example()

Nui.state.snapshot:Set({ count = 1, message = "Ready" })
Nui.state.snapshot:Patch({ message = "Updated" })
Nui.state.preferences:Watch(function(preferences)
    print(preferences.compact)
end)
Nui.events.toast:Emit({ level = "success", message = "Saved" })
local result = Nui.rpc.confirm:Call({ message = "Continue?" })

State đồng bộ không có owner: Lua và NUI đều có thể Get/Set/Watch, và object state có Patch. Lua bridge là canonical revision sequencer; write hợp lệ được xử lý sau cùng sẽ thắng. UI bình thường dùng decorated store, MobX binding được thiết lập tự động:

const example = appRuntime.stores.example;

example.preferences = {compact: true};
console.log(example.snapshot.message);

await appRuntime.flush(); // optional: wait for queued browser writes

Assignment được validate đồng bộ và cập nhật optimistic. Browser serialize write theo từng state; transport failure được báo qua runtime onError. Khi cần API cấp thấp:

const exampleNui = appRuntime.nui.nui.example;

const subscription = exampleNui.state.snapshot.subscribe(console.log);
await exampleNui.state.preferences.patch({compact: true});
await exampleNui.events.selected.emit({id: 'item-1'});
const result = await exampleNui.rpc.increment.call({amount: 1});

subscription.dispose();

Protocol v2 gửi browser state commands với requestIdsessionId, không gửi revision. Lua chấp nhận write, cấp revision và phát full state.snapshot. Startup chỉ hoàn tất sau core.ready, initial snapshots và barrier core.synced của cùng session.

Client/server transport và public exports cũng chỉ expose endpoint có trong contract:

-- client
local snapshot = LR.Net.Example.rpc.setMessage:Call({ message = "Hello" })

-- server
LR.Net.Example.rpc.setMessage:Handle({
    Authorize = function(playerSource, request)
        return playerSource > 0
    end,
    Execute = function(playerSource, request)
        return { count = 1, message = request.message }
    end,
})

-- provider
LR.Public.Example:Register({
    getSnapshot = function(request)
        return { count = 1, message = "Ready" }
    end,
})

-- consumer
local external = LR.External.Example("other_resource")
local result = external:GetSnapshot({})

Raw FiveM events, NUI callbacks và exports chỉ được dùng trong transport core hoặc exact framework bridge boundary. Module code không dùng event string hoặc method string tùy ý.

Quality gates

pnpm check:contracts   # generated artifacts deterministic and committed
pnpm check:cfx         # pinned Cfx snapshot and side declarations have no drift
pnpm update:cfx        # explicit upstream snapshot refresh; normal build stays offline
pnpm typecheck         # contracts, tooling and strict NUI TypeScript
pnpm test:nui-store    # Store/State metadata, validation and registry behavior
pnpm lint
pnpm test:coverage     # authored core/contracts/bridge >= 80%
pnpm test:e2e          # Chromium app boot, handshake, state render and close RPC
pnpm test:lua          # lifecycle, schema, transport and security specs
pnpm check:luals       # client/server/shared real LuaLS diagnostics
pnpm test:security     # forbidden raw APIs and legacy/RCE patterns
pnpm verify:manifest
pnpm audit:prod
pnpm build
pnpm verify            # toàn bộ gate trên
pnpm package           # deterministic release staging + zip

Trước khi release cần chạy checklist bằng FXServer và client thật trong docs/RELEASE.md. Automated mocks không thay thế kiểm tra focus, restart resource, authorization và cleanup trong runtime FiveM thật.

Tài liệu