@laot/nuix 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -41
- package/dist/index.cjs +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,56 +4,103 @@
|
|
|
4
4
|
[](https://github.com/laot7490/nuix/blob/main/LICENSE)
|
|
5
5
|
[](https://bundlephobia.com/package/@laot/nuix)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A type-safe TypeScript helper library for FiveM NUI development. Wraps the most common NUI patterns — fetching data from Lua, listening for messages, formatting strings, and handling translations — in a clean, fully typed API. Zero runtime dependencies, works with any frontend framework.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Install](#install)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Event Maps](#1-event-maps)
|
|
16
|
+
- [fetchNui](#2-fetchnui--typed-lua-callbacks)
|
|
17
|
+
- [onNuiMessage](#3-onnuimessage--listening-to-lua)
|
|
18
|
+
- [luaFormat](#4-luaformat--string-formatting)
|
|
19
|
+
- [Translator (Global)](#5-translator-global)
|
|
20
|
+
- [Translator (Isolated)](#6-translator-isolated)
|
|
21
|
+
- [Debug Mode](#7-debug-mode)
|
|
22
|
+
- [Mock Data](#8-mock-data-local-development)
|
|
23
|
+
- [isEnvBrowser](#9-isenvbrowser--environment-detection)
|
|
24
|
+
- [disableMockInGame](#10-disablemockingame--mock-control-in-fivem)
|
|
25
|
+
- [Lua Side](#lua-side)
|
|
26
|
+
- [API Reference](#api-reference)
|
|
27
|
+
- [Build](#build)
|
|
28
|
+
- [License](#license)
|
|
29
|
+
|
|
30
|
+
---
|
|
8
31
|
|
|
9
32
|
## Install
|
|
10
33
|
|
|
11
34
|
```bash
|
|
35
|
+
# pick your package manager
|
|
12
36
|
npm install @laot/nuix
|
|
13
37
|
pnpm add @laot/nuix
|
|
14
38
|
yarn add @laot/nuix
|
|
15
39
|
bun add @laot/nuix
|
|
16
40
|
```
|
|
17
41
|
|
|
42
|
+
---
|
|
43
|
+
|
|
18
44
|
## Quick Start
|
|
19
45
|
|
|
20
|
-
### 1.
|
|
46
|
+
### 1. Event Maps
|
|
47
|
+
|
|
48
|
+
Before using anything, you'll want to define your events. NUIX uses these maps to infer the exact types for both data you send and responses you get back. You'll typically have two separate maps:
|
|
49
|
+
|
|
50
|
+
- **Callback events** — for `fetchNui` calls. Your TS code sends data to Lua, Lua processes it and sends a response back.
|
|
51
|
+
- **Message events** — for `onNuiMessage` listeners. Lua pushes data to TS via `SendNUIMessage`, no response needed.
|
|
21
52
|
|
|
22
53
|
```ts
|
|
23
54
|
import type { NuiEventMap } from "@laot/nuix";
|
|
24
55
|
|
|
25
|
-
//
|
|
56
|
+
// Things you ASK Lua for (request → response)
|
|
26
57
|
interface CallbackEvents extends NuiEventMap {
|
|
27
58
|
getPlayer: { data: { id: number }; response: { name: string; level: number } };
|
|
28
59
|
sendNotify: { data: { message: string }; response: void };
|
|
29
60
|
}
|
|
30
61
|
|
|
31
|
-
// Lua
|
|
62
|
+
// Things Lua TELLS you about (one-way push)
|
|
32
63
|
interface MessageEvents extends NuiEventMap {
|
|
33
64
|
showMenu: { data: { items: string[] }; response: void };
|
|
34
65
|
hideMenu: { data: void; response: void };
|
|
35
66
|
}
|
|
36
67
|
```
|
|
37
68
|
|
|
38
|
-
|
|
69
|
+
> Keeping them separate isn't mandatory, but it makes your code way easier to reason about — you'll always know which events go where.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### 2. `fetchNui` — Typed Lua Callbacks
|
|
74
|
+
|
|
75
|
+
`createFetchNui` gives you a typed function that POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on the Lua side. The resource name is automatically grabbed from FiveM's `GetParentResourceName()`.
|
|
39
76
|
|
|
40
77
|
```ts
|
|
41
78
|
import { createFetchNui } from "@laot/nuix";
|
|
42
79
|
|
|
43
80
|
const fetchNui = createFetchNui<CallbackEvents>();
|
|
44
81
|
|
|
82
|
+
// fully typed — player is { name: string; level: number }
|
|
45
83
|
const player = await fetchNui("getPlayer", { id: 1 });
|
|
46
|
-
console.log(player.name);
|
|
84
|
+
console.log(player.name, player.level);
|
|
47
85
|
|
|
86
|
+
// void response — you're just notifying Lua, no return value
|
|
48
87
|
await fetchNui("sendNotify", { message: "Hello!" });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
You can also set a **timeout** to avoid hanging forever if the Lua callback never responds:
|
|
49
91
|
|
|
50
|
-
|
|
92
|
+
```ts
|
|
51
93
|
const data = await fetchNui("getPlayer", { id: 2 }, { timeout: 5000 });
|
|
94
|
+
// rejects with "[NUIX] fetchNui("getPlayer") timed out after 5000ms" if no response
|
|
52
95
|
```
|
|
53
96
|
|
|
54
|
-
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### 3. `onNuiMessage` — Listening to Lua
|
|
55
100
|
|
|
56
|
-
|
|
101
|
+
Listens for messages from Lua's `SendNUIMessage`. There are two ways to use it:
|
|
102
|
+
|
|
103
|
+
**Switch-case** — one listener that handles every action:
|
|
57
104
|
|
|
58
105
|
```ts
|
|
59
106
|
import { onNuiMessage } from "@laot/nuix";
|
|
@@ -69,10 +116,11 @@ const unsub = onNuiMessage<MessageEvents>((action, data) => {
|
|
|
69
116
|
}
|
|
70
117
|
});
|
|
71
118
|
|
|
72
|
-
|
|
119
|
+
// when you're done listening
|
|
120
|
+
unsub();
|
|
73
121
|
```
|
|
74
122
|
|
|
75
|
-
**Per-action** —
|
|
123
|
+
**Per-action** — filters by action name, and `data` is fully typed automatically:
|
|
76
124
|
|
|
77
125
|
```ts
|
|
78
126
|
const unsub = onNuiMessage<MessageEvents, "showMenu">("showMenu", (data) => {
|
|
@@ -80,7 +128,20 @@ const unsub = onNuiMessage<MessageEvents, "showMenu">("showMenu", (data) => {
|
|
|
80
128
|
});
|
|
81
129
|
```
|
|
82
130
|
|
|
83
|
-
|
|
131
|
+
Both overloads return an `UnsubscribeFn` — just call it to remove the event listener.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### 4. `luaFormat` — String Formatting
|
|
136
|
+
|
|
137
|
+
A small utility that formats strings using Lua-style placeholders. Handles `null` and `undefined` safely instead of crashing.
|
|
138
|
+
|
|
139
|
+
| Specifier | What it does |
|
|
140
|
+
|-----------|--------------------------------------|
|
|
141
|
+
| `%s` | String (null/undefined → `""`) |
|
|
142
|
+
| `%d` / `%i` | Integer (floors the value, NaN → `0`) |
|
|
143
|
+
| `%f` | Float (NaN → `0`) |
|
|
144
|
+
| `%%` | Literal `%` sign |
|
|
84
145
|
|
|
85
146
|
```ts
|
|
86
147
|
import { luaFormat } from "@laot/nuix";
|
|
@@ -90,11 +151,18 @@ luaFormat("Hello %s, you are level %d", "Laot", 42);
|
|
|
90
151
|
|
|
91
152
|
luaFormat("Accuracy: %f%%", 99.5);
|
|
92
153
|
// → "Accuracy: 99.5%"
|
|
154
|
+
|
|
155
|
+
luaFormat("Safe: %s %d", undefined, NaN);
|
|
156
|
+
// → "Safe: 0"
|
|
93
157
|
```
|
|
94
158
|
|
|
159
|
+
---
|
|
160
|
+
|
|
95
161
|
### 5. Translator (Global)
|
|
96
162
|
|
|
97
|
-
|
|
163
|
+
A global translation system built on top of `luaFormat`. The idea is simple: Lua sends locale data once (usually on resource start), you register it, and then use `_U()` anywhere in your UI to get translated strings.
|
|
164
|
+
|
|
165
|
+
**Registration:**
|
|
98
166
|
|
|
99
167
|
```ts
|
|
100
168
|
import { registerLocales, _U, onNuiMessage } from "@laot/nuix";
|
|
@@ -108,35 +176,44 @@ interface Events extends NuiEventMap {
|
|
|
108
176
|
onNuiMessage<Events>((action, data) => {
|
|
109
177
|
switch (action) {
|
|
110
178
|
case "setLocales":
|
|
111
|
-
registerLocales(data);
|
|
179
|
+
registerLocales(data); // store the locale map globally
|
|
112
180
|
break;
|
|
113
181
|
case "showMenu":
|
|
114
182
|
openMenu(data.items);
|
|
115
183
|
break;
|
|
116
184
|
}
|
|
117
185
|
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Usage — anywhere in your app:**
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// assuming Lua sent: { ui: { greeting: "Hello %s!", level: "Level %d" } }
|
|
118
192
|
|
|
119
|
-
// Use _U anywhere
|
|
120
193
|
_U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
|
|
121
194
|
_U("ui.level", "Lv.", 42); // → "Level 42"
|
|
122
|
-
_U("missing.key", "Fallback"); // → "Fallback"
|
|
195
|
+
_U("missing.key", "Fallback"); // → "Fallback" (key not found, returns fallback)
|
|
123
196
|
```
|
|
124
197
|
|
|
125
|
-
|
|
198
|
+
**Adding more translations later** without overwriting existing ones:
|
|
126
199
|
|
|
127
200
|
```ts
|
|
128
201
|
import { extendLocales } from "@laot/nuix";
|
|
129
202
|
|
|
130
203
|
extendLocales({ ui: { subtitle: "Overview" } });
|
|
131
|
-
//
|
|
204
|
+
// merges into the existing locale map — won't touch other keys
|
|
132
205
|
```
|
|
133
206
|
|
|
207
|
+
> `_U` uses dot notation. `"ui.greeting"` looks up `locales.ui.greeting` under the hood.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
134
211
|
### 6. Translator (Isolated)
|
|
135
212
|
|
|
136
|
-
If you need a
|
|
213
|
+
If you need a translator that's completely independent from the global `_U` — maybe a component with its own locale scope — use `createTranslator`:
|
|
137
214
|
|
|
138
215
|
```ts
|
|
139
|
-
import { createTranslator
|
|
216
|
+
import { createTranslator } from "@laot/nuix";
|
|
140
217
|
|
|
141
218
|
const _T = createTranslator({
|
|
142
219
|
locales: {
|
|
@@ -147,34 +224,50 @@ const _T = createTranslator({
|
|
|
147
224
|
|
|
148
225
|
_T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
|
|
149
226
|
_T("level", "MISSING", 42); // → "Level 42"
|
|
227
|
+
_T("no.key", "Not found"); // → "Not found"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
There's also `mergeLocales` if you need to deep-merge locale records manually:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { mergeLocales } from "@laot/nuix";
|
|
150
234
|
|
|
151
|
-
// Deep-merge multiple locale records
|
|
152
235
|
const base = { ui: { greeting: "Hello %s!" } };
|
|
153
236
|
const patch = { ui: { greeting: "Hey %s, welcome back!" } };
|
|
237
|
+
|
|
154
238
|
const merged = mergeLocales(base, patch);
|
|
239
|
+
// merged.ui.greeting → "Hey %s, welcome back!"
|
|
155
240
|
```
|
|
156
241
|
|
|
242
|
+
---
|
|
243
|
+
|
|
157
244
|
### 7. Debug Mode
|
|
158
245
|
|
|
159
|
-
|
|
246
|
+
Pass `debug: true` to `createFetchNui` and every call will be logged to the console with the `[NUIX]` prefix. Super useful during development:
|
|
160
247
|
|
|
161
248
|
```ts
|
|
162
249
|
const fetchNui = createFetchNui<CallbackEvents>({ debug: true });
|
|
163
250
|
|
|
164
251
|
await fetchNui("getPlayer", { id: 1 });
|
|
252
|
+
// Console:
|
|
165
253
|
// [NUIX] → getPlayer { id: 1 }
|
|
166
254
|
// [NUIX] ← getPlayer { name: "Laot", level: 42 }
|
|
167
255
|
```
|
|
168
256
|
|
|
257
|
+
---
|
|
258
|
+
|
|
169
259
|
### 8. Mock Data (Local Development)
|
|
170
260
|
|
|
171
|
-
|
|
261
|
+
When you're building your UI outside of FiveM (like in a regular browser with `npm run dev`), there's no Lua backend to respond to your `fetchNui` calls. That's where `mockData` comes in — it returns pre-defined responses without making any HTTP requests.
|
|
172
262
|
|
|
173
263
|
```ts
|
|
174
264
|
const fetchNui = createFetchNui<CallbackEvents>({
|
|
175
265
|
debug: true,
|
|
176
266
|
mockData: {
|
|
267
|
+
// static response — just return this object every time
|
|
177
268
|
getPlayer: { name: "DevPlayer", level: 99 },
|
|
269
|
+
|
|
270
|
+
// dynamic response — receive the data, return something based on it
|
|
178
271
|
sendNotify: (data) => {
|
|
179
272
|
console.log("Mock notification:", data.message);
|
|
180
273
|
},
|
|
@@ -182,47 +275,122 @@ const fetchNui = createFetchNui<CallbackEvents>({
|
|
|
182
275
|
});
|
|
183
276
|
|
|
184
277
|
const player = await fetchNui("getPlayer", { id: 1 });
|
|
278
|
+
// Console:
|
|
185
279
|
// [NUIX] → getPlayer { id: 1 }
|
|
186
280
|
// [NUIX] ← getPlayer (mock) { name: "DevPlayer", level: 99 }
|
|
187
281
|
```
|
|
188
282
|
|
|
189
|
-
|
|
283
|
+
> Works great combined with `debug: true` — you can see exactly what's being sent and received in the console.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### 9. `isEnvBrowser` — Environment Detection
|
|
288
|
+
|
|
289
|
+
Returns `true` when running outside FiveM (regular browser). Uses the absence of FiveM's `invokeNative` bridge to detect the environment.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { isEnvBrowser } from "@laot/nuix";
|
|
293
|
+
|
|
294
|
+
if (isEnvBrowser()) {
|
|
295
|
+
// Running in dev browser — skip game-only logic
|
|
296
|
+
console.log("Dev mode");
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
> **Note:** `fetchNui` already uses this internally — if you're in a browser and no `mockData` is configured for the event, it throws a clear error instead of making a doomed HTTP request.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### 10. `disableMockInGame` — Mock Control in FiveM
|
|
305
|
+
|
|
306
|
+
By default, `mockData` is used everywhere — both in the browser and inside FiveM. If you want mocks to only work during local development but use real Lua callbacks inside the game, set `disableMockInGame: true`:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
const fetchNui = createFetchNui<CallbackEvents>({
|
|
310
|
+
disableMockInGame: true,
|
|
311
|
+
mockData: {
|
|
312
|
+
getPlayer: { name: "DevPlayer", level: 99 },
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// In browser → returns mock { name: "DevPlayer", level: 99 }
|
|
317
|
+
// In FiveM → calls real RegisterNUICallback("getPlayer", ...)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
| Environment | `disableMockInGame: false` (default) | `disableMockInGame: true` |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| Browser + mock exists | Mock response ✅ | Mock response ✅ |
|
|
323
|
+
| Browser + no mock | Error thrown ❌ | Error thrown ❌ |
|
|
324
|
+
| FiveM + mock exists | Mock response | **Real fetch** |
|
|
325
|
+
| FiveM + no mock | Real fetch | Real fetch |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Lua Side
|
|
330
|
+
|
|
331
|
+
Here's how the Lua side connects to everything above:
|
|
190
332
|
|
|
191
333
|
```lua
|
|
192
|
-
--
|
|
334
|
+
-- Responds to fetchNui("getPlayer", { id = ... })
|
|
193
335
|
RegisterNUICallback("getPlayer", function(data, cb)
|
|
194
336
|
local player = GetPlayerData(data.id)
|
|
195
337
|
cb({ name = player.name, level = player.level })
|
|
196
338
|
end)
|
|
197
339
|
|
|
198
|
-
--
|
|
340
|
+
-- Pushes a message to onNuiMessage listeners
|
|
199
341
|
SendNUIMessage({ action = "showMenu", data = { items = {"Pistol", "Rifle"} } })
|
|
200
342
|
|
|
201
|
-
--
|
|
343
|
+
-- Sends locale data for registerLocales
|
|
202
344
|
SendNUIMessage({ action = "setLocales", data = Locales })
|
|
203
345
|
```
|
|
204
346
|
|
|
347
|
+
---
|
|
348
|
+
|
|
205
349
|
## API Reference
|
|
206
350
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
|
210
|
-
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
215
|
-
| `
|
|
216
|
-
| `
|
|
217
|
-
| `
|
|
351
|
+
### Functions
|
|
352
|
+
|
|
353
|
+
| Export | Description |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `createFetchNui<TMap>(options?)` | Returns a typed `fetchNui` function. Supports `debug`, `mockData`, and `disableMockInGame` options. |
|
|
356
|
+
| `isEnvBrowser()` | Returns `true` when running outside FiveM (regular browser). |
|
|
357
|
+
| `onNuiMessage<TMap>(handler)` | Listens to all NUI messages — use with a switch-case. |
|
|
358
|
+
| `onNuiMessage<TMap, K>(action, handler)` | Listens to a single action — `data` is automatically typed. |
|
|
359
|
+
| `luaFormat(template, ...args)` | Lua-style string formatter with `%s` / `%d` / `%f` support. |
|
|
360
|
+
| `registerLocales(locales)` | Sets the global locale map (replaces the current one). |
|
|
361
|
+
| `extendLocales(...records)` | Merges new entries into the existing global locale map. |
|
|
362
|
+
| `_U(key, fallback, ...args)` | Global translator — reads from the registered locale map. |
|
|
363
|
+
| `createTranslator(options)` | Returns an isolated translator function with its own locale scope. |
|
|
364
|
+
| `mergeLocales(...records)` | Deep-merges multiple locale records into one. |
|
|
365
|
+
|
|
366
|
+
### Types
|
|
367
|
+
|
|
368
|
+
| Export | Description |
|
|
369
|
+
|---|---|
|
|
370
|
+
| `NuiEventMap` | Base interface for defining event maps. |
|
|
371
|
+
| `NuiMessagePayload<TData>` | Shape of `SendNUIMessage` payloads (`{ action, data }`). |
|
|
372
|
+
| `FetchNuiOptions` | Per-call options for `fetchNui` (e.g. `timeout`). |
|
|
373
|
+
| `FetchNuiFactoryOptions<TMap>` | Config for `createFetchNui` (`debug`, `mockData`, `disableMockInGame`). |
|
|
374
|
+
| `LocaleRecord` | Flat or nested string map used for translations. |
|
|
375
|
+
| `TranslatorOptions` | Config for `createTranslator`. |
|
|
376
|
+
| `TranslatorFn` | Translator function signature (`(key, fallback, ...args) => string`). |
|
|
377
|
+
| `FormatArg` | Accepted argument types for `luaFormat` (`string \| number \| boolean \| null \| undefined`). |
|
|
378
|
+
| `UnsubscribeFn` | Cleanup function returned by `onNuiMessage`. |
|
|
379
|
+
| `NuiMessageHandler<TData>` | Callback type for NUI message listeners. |
|
|
380
|
+
|
|
381
|
+
---
|
|
218
382
|
|
|
219
383
|
## Build
|
|
220
384
|
|
|
221
385
|
```bash
|
|
222
|
-
npm run build # ESM + CJS + .d.ts
|
|
223
|
-
npm run typecheck
|
|
386
|
+
npm run build # outputs ESM + CJS + .d.ts to dist/
|
|
387
|
+
npm run typecheck # tsc --noEmit
|
|
224
388
|
```
|
|
225
389
|
|
|
390
|
+
Requires Node.js ≥ 18.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
226
394
|
## License
|
|
227
395
|
|
|
228
|
-
MIT
|
|
396
|
+
[MIT](LICENSE) © LAOT
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
createFetchNui: () => createFetchNui,
|
|
25
25
|
createTranslator: () => createTranslator,
|
|
26
26
|
extendLocales: () => extendLocales,
|
|
27
|
+
isEnvBrowser: () => isEnvBrowser,
|
|
27
28
|
luaFormat: () => luaFormat,
|
|
28
29
|
mergeLocales: () => mergeLocales,
|
|
29
30
|
onNuiMessage: () => onNuiMessage,
|
|
@@ -32,6 +33,9 @@ __export(index_exports, {
|
|
|
32
33
|
module.exports = __toCommonJS(index_exports);
|
|
33
34
|
|
|
34
35
|
// src/client.ts
|
|
36
|
+
function isEnvBrowser() {
|
|
37
|
+
return typeof window !== "undefined" && !window.invokeNative;
|
|
38
|
+
}
|
|
35
39
|
function getResourceName() {
|
|
36
40
|
if (typeof window !== "undefined" && window.GetParentResourceName) {
|
|
37
41
|
return window.GetParentResourceName();
|
|
@@ -41,12 +45,14 @@ function getResourceName() {
|
|
|
41
45
|
function createFetchNui(factoryOptions) {
|
|
42
46
|
const debug = factoryOptions?.debug ?? false;
|
|
43
47
|
const mockData = factoryOptions?.mockData;
|
|
48
|
+
const disableMockInGame = factoryOptions?.disableMockInGame ?? false;
|
|
44
49
|
return async function fetchNui(event, ...args) {
|
|
45
50
|
const [data, options] = args;
|
|
46
51
|
if (debug) {
|
|
47
52
|
console.log(`[NUIX] \u2192 ${event}`, data ?? {});
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
const useMock = mockData && event in mockData && (isEnvBrowser() || !disableMockInGame);
|
|
55
|
+
if (useMock) {
|
|
50
56
|
const mock = mockData[event];
|
|
51
57
|
if (mock === void 0) {
|
|
52
58
|
throw new Error(`[NUIX] Mock data for "${event}" is undefined`);
|
|
@@ -57,6 +63,11 @@ function createFetchNui(factoryOptions) {
|
|
|
57
63
|
}
|
|
58
64
|
return result;
|
|
59
65
|
}
|
|
66
|
+
if (isEnvBrowser()) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`[NUIX] fetchNui("${event}") called in browser environment without mock data. Provide mockData in createFetchNui() options for local development.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
60
71
|
const url = `https://${getResourceName()}/${event}`;
|
|
61
72
|
const controller = options?.timeout ? new AbortController() : void 0;
|
|
62
73
|
let timeoutId;
|
|
@@ -184,6 +195,7 @@ function mergeLocales(...records) {
|
|
|
184
195
|
createFetchNui,
|
|
185
196
|
createTranslator,
|
|
186
197
|
extendLocales,
|
|
198
|
+
isEnvBrowser,
|
|
187
199
|
luaFormat,
|
|
188
200
|
mergeLocales,
|
|
189
201
|
onNuiMessage,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["// ─── Types ───\r\nexport type {\r\n\tNuiEventMap,\r\n\tNuiMessagePayload,\r\n\tFetchNuiOptions,\r\n\tFetchNuiFactoryOptions,\r\n\tLocaleRecord,\r\n\tTranslatorOptions,\r\n\tTranslatorFn,\r\n\tFormatArg,\r\n\tUnsubscribeFn,\r\n\tNuiMessageHandler,\r\n} from \"./types\";\r\n\r\n// ─── Client ───\r\nexport { createFetchNui } from \"./client\";\r\n\r\n// ─── Listener ───\r\nexport { onNuiMessage } from \"./listener\";\r\n\r\n// ─── Utils ───\r\nexport { luaFormat, createTranslator, mergeLocales, registerLocales, extendLocales, _U } from \"./utils\";\r\n","import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a typed `fetchNui` function for your event map.\r\n * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { data: { id: number }; response: { name: string } };\r\n * notify: { data: { msg: string }; response: void };\r\n * }\r\n *\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n *\r\n * // With mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (data) => { console.log(\"Mock:\", data.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\t...args: TMap[K][\"data\"] extends void\r\n\t\t\t? [data?: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tconst [data, options] = args;\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\r\n\t\tif (mockData && event in mockData) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (data: TMap[K][\"data\"]) => TMap[K][\"response\"])(data as TMap[K][\"data\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`).\r\n *\r\n * **Per-action** — filters by action name, `data` is fully typed:\r\n * ```ts\r\n * onNuiMessage<Events, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // ✅ typed as string[]\r\n * });\r\n * ```\r\n *\r\n * **Switch-case** — single listener for all actions:\r\n * ```ts\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * case \"showMenu\":\r\n * openMenu(data.items);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\n\r\n// Per-action overload\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"data\"]>,\r\n): UnsubscribeFn;\r\n\r\n// Switch-case overload\r\nexport function onNuiMessage<TMap extends NuiEventMap>(\r\n\thandler: (action: keyof TMap & string, data: any) => void,\r\n): UnsubscribeFn;\r\n\r\n// Implementation\r\nexport function onNuiMessage(\r\n\tactionOrHandler: string | ((action: string, data: unknown) => void),\r\n\thandler?: (data: unknown) => void,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (!payload.action) return;\r\n\r\n\t\tif (typeof actionOrHandler === \"string\") {\r\n\t\t\tif (payload.action !== actionOrHandler) return;\r\n\t\t\thandler!(payload.data);\r\n\t\t} else {\r\n\t\t\tactionOrHandler(payload.action, payload.data);\r\n\t\t}\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` / `%i` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates an isolated translator bound to a specific locale record.\r\n * Use this when you need a separate translator instance, independent of the global `_U`.\r\n *\r\n * @example\r\n * ```ts\r\n * const _T = createTranslator({\r\n * locales: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * });\r\n *\r\n * _T(\"greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _T(\"level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _T(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Global Locale Registry ───\r\n\r\nlet _locales: LocaleRecord = {};\r\n\r\n/**\r\n * Sets the global locale map. Call this when Lua sends locale data to the NUI.\r\n *\r\n * @example\r\n * ```ts\r\n * // Lua side:\r\n * // SendNUIMessage({ action = \"setLocales\", data = locales })\r\n *\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport function registerLocales(locales: LocaleRecord): void {\r\n\t_locales = locales;\r\n}\r\n\r\n/**\r\n * Merges new entries into the current global locale map without replacing it.\r\n *\r\n * @example\r\n * ```ts\r\n * registerLocales({ ui: { title: \"Dashboard\" } });\r\n * extendLocales({ ui: { subtitle: \"Overview\" } });\r\n *\r\n * _U(\"ui.title\", \"\"); // → \"Dashboard\"\r\n * _U(\"ui.subtitle\", \"\"); // → \"Overview\"\r\n * ```\r\n */\r\nexport function extendLocales(...records: LocaleRecord[]): void {\r\n\t_locales = mergeLocales(_locales, ...records);\r\n}\r\n\r\n/**\r\n * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.\r\n *\r\n * @param key Dot-notated key, e.g. `\"ui.greeting\"` or flat `\"title\"`\r\n * @param fallback Returned as-is when the key doesn't exist\r\n * @param args Values for `%s`, `%d`, `%f` placeholders\r\n *\r\n * @example\r\n * ```ts\r\n * import { registerLocales, _U } from \"@laot/nuix\";\r\n *\r\n * // After Lua sends locales:\r\n * // { ui: { greeting: \"Hello %s!\", level: \"Level %d\" } }\r\n *\r\n * _U(\"ui.greeting\", \"Hi\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"ui.level\", \"Lv.\", 42); // → \"Level 42\"\r\n * _U(\"missing.key\", \"Fallback\"); // → \"Fallback\"\r\n * ```\r\n */\r\nexport function _U(key: string, fallback: string, ...args: FormatArg[]): string {\r\n\tconst template = resolveKey(_locales, key);\r\n\tif (template === undefined) return fallback;\r\n\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (\r\n\t\t\t\tvalue !== null &&\r\n\t\t\t\texisting !== null &&\r\n\t\t\t\ttypeof value === \"object\" &&\r\n\t\t\t\ttypeof existing === \"object\" &&\r\n\t\t\t\texisting !== undefined\r\n\t\t\t) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["// ─── Types ───\r\nexport type {\r\n\tNuiEventMap,\r\n\tNuiMessagePayload,\r\n\tFetchNuiOptions,\r\n\tFetchNuiFactoryOptions,\r\n\tLocaleRecord,\r\n\tTranslatorOptions,\r\n\tTranslatorFn,\r\n\tFormatArg,\r\n\tUnsubscribeFn,\r\n\tNuiMessageHandler,\r\n} from \"./types\";\r\n\r\n// ─── Client ───\r\nexport { createFetchNui, isEnvBrowser } from \"./client\";\r\n\r\n// ─── Listener ───\r\nexport { onNuiMessage } from \"./listener\";\r\n\r\n// ─── Utils ───\r\nexport { luaFormat, createTranslator, mergeLocales, registerLocales, extendLocales, _U } from \"./utils\";\r\n","import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Environment Detection ───\r\n\r\n/**\r\n * Returns `true` when the UI is running inside a regular browser (outside FiveM).\r\n * Checks for the absence of FiveM's `invokeNative` bridge on the `window` object.\r\n *\r\n * Use this to gate any logic that should only run inside the game client.\r\n *\r\n * @example\r\n * ```ts\r\n * if (isEnvBrowser()) {\r\n * // Running in dev browser — skip game-only logic\r\n * }\r\n * ```\r\n */\r\nexport function isEnvBrowser(): boolean {\r\n\treturn typeof window !== \"undefined\" && !(window as any).invokeNative;\r\n}\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a typed `fetchNui` function for your event map.\r\n * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { data: { id: number }; response: { name: string } };\r\n * notify: { data: { msg: string }; response: void };\r\n * }\r\n *\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n *\r\n * // With mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (data) => { console.log(\"Mock:\", data.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\tconst disableMockInGame = factoryOptions?.disableMockInGame ?? false;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\t...args: TMap[K][\"data\"] extends void\r\n\t\t\t? [data?: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tconst [data, options] = args;\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\t\t// Skip mocks inside FiveM when disableMockInGame is true\r\n\r\n\t\tconst useMock = mockData && event in mockData && (isEnvBrowser() || !disableMockInGame);\r\n\t\tif (useMock) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (data: TMap[K][\"data\"]) => TMap[K][\"response\"])(data as TMap[K][\"data\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Browser Guard ───\r\n\r\n\t\tif (isEnvBrowser()) {\r\n\t\t\tthrow new Error(\r\n\t\t\t\t`[NUIX] fetchNui(\"${event}\") called in browser environment without mock data. ` +\r\n\t\t\t\t\t`Provide mockData in createFetchNui() options for local development.`,\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`).\r\n *\r\n * **Per-action** — filters by action name, `data` is fully typed:\r\n * ```ts\r\n * onNuiMessage<Events, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // ✅ typed as string[]\r\n * });\r\n * ```\r\n *\r\n * **Switch-case** — single listener for all actions:\r\n * ```ts\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * case \"showMenu\":\r\n * openMenu(data.items);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\n\r\n// Per-action overload\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"data\"]>,\r\n): UnsubscribeFn;\r\n\r\n// Switch-case overload\r\nexport function onNuiMessage<TMap extends NuiEventMap>(\r\n\thandler: (action: keyof TMap & string, data: any) => void,\r\n): UnsubscribeFn;\r\n\r\n// Implementation\r\nexport function onNuiMessage(\r\n\tactionOrHandler: string | ((action: string, data: unknown) => void),\r\n\thandler?: (data: unknown) => void,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (!payload.action) return;\r\n\r\n\t\tif (typeof actionOrHandler === \"string\") {\r\n\t\t\tif (payload.action !== actionOrHandler) return;\r\n\t\t\thandler!(payload.data);\r\n\t\t} else {\r\n\t\t\tactionOrHandler(payload.action, payload.data);\r\n\t\t}\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` / `%i` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates an isolated translator bound to a specific locale record.\r\n * Use this when you need a separate translator instance, independent of the global `_U`.\r\n *\r\n * @example\r\n * ```ts\r\n * const _T = createTranslator({\r\n * locales: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * });\r\n *\r\n * _T(\"greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _T(\"level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _T(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Global Locale Registry ───\r\n\r\nlet _locales: LocaleRecord = {};\r\n\r\n/**\r\n * Sets the global locale map. Call this when Lua sends locale data to the NUI.\r\n *\r\n * @example\r\n * ```ts\r\n * // Lua side:\r\n * // SendNUIMessage({ action = \"setLocales\", data = locales })\r\n *\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport function registerLocales(locales: LocaleRecord): void {\r\n\t_locales = locales;\r\n}\r\n\r\n/**\r\n * Merges new entries into the current global locale map without replacing it.\r\n *\r\n * @example\r\n * ```ts\r\n * registerLocales({ ui: { title: \"Dashboard\" } });\r\n * extendLocales({ ui: { subtitle: \"Overview\" } });\r\n *\r\n * _U(\"ui.title\", \"\"); // → \"Dashboard\"\r\n * _U(\"ui.subtitle\", \"\"); // → \"Overview\"\r\n * ```\r\n */\r\nexport function extendLocales(...records: LocaleRecord[]): void {\r\n\t_locales = mergeLocales(_locales, ...records);\r\n}\r\n\r\n/**\r\n * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.\r\n *\r\n * @param key Dot-notated key, e.g. `\"ui.greeting\"` or flat `\"title\"`\r\n * @param fallback Returned as-is when the key doesn't exist\r\n * @param args Values for `%s`, `%d`, `%f` placeholders\r\n *\r\n * @example\r\n * ```ts\r\n * import { registerLocales, _U } from \"@laot/nuix\";\r\n *\r\n * // After Lua sends locales:\r\n * // { ui: { greeting: \"Hello %s!\", level: \"Level %d\" } }\r\n *\r\n * _U(\"ui.greeting\", \"Hi\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"ui.level\", \"Lv.\", 42); // → \"Level 42\"\r\n * _U(\"missing.key\", \"Fallback\"); // → \"Fallback\"\r\n * ```\r\n */\r\nexport function _U(key: string, fallback: string, ...args: FormatArg[]): string {\r\n\tconst template = resolveKey(_locales, key);\r\n\tif (template === undefined) return fallback;\r\n\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (\r\n\t\t\t\tvalue !== null &&\r\n\t\t\t\texisting !== null &&\r\n\t\t\t\ttypeof value === \"object\" &&\r\n\t\t\t\ttypeof existing === \"object\" &&\r\n\t\t\t\texisting !== undefined\r\n\t\t\t) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extend this to map your NUI
|
|
3
|
-
* Both `fetchNui` and `onNuiMessage` use this map for
|
|
2
|
+
* Extend this to map your NUI event names to their data/response shapes.
|
|
3
|
+
* Both `fetchNui` and `onNuiMessage` use this map for type inference.
|
|
4
4
|
*
|
|
5
5
|
* @example
|
|
6
6
|
* ```ts
|
|
@@ -59,6 +59,12 @@ interface FetchNuiFactoryOptions<TMap extends NuiEventMap> {
|
|
|
59
59
|
mockData?: {
|
|
60
60
|
[K in keyof TMap]?: TMap[K]["response"] | ((data: TMap[K]["data"]) => TMap[K]["response"]);
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* When `true`, mock data is ignored inside FiveM and real NUI callbacks are used instead.
|
|
64
|
+
* Mocks will still work in the browser for local development.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
disableMockInGame?: boolean;
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Flat strings or nested objects. Nested keys use dot-notation (`"ui.greeting"`).
|
|
@@ -105,6 +111,20 @@ type UnsubscribeFn = () => void;
|
|
|
105
111
|
/** Callback invoked when a matching NUI message arrives. */
|
|
106
112
|
type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
107
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Returns `true` when the UI is running inside a regular browser (outside FiveM).
|
|
116
|
+
* Checks for the absence of FiveM's `invokeNative` bridge on the `window` object.
|
|
117
|
+
*
|
|
118
|
+
* Use this to gate any logic that should only run inside the game client.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* if (isEnvBrowser()) {
|
|
123
|
+
* // Running in dev browser — skip game-only logic
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function isEnvBrowser(): boolean;
|
|
108
128
|
/**
|
|
109
129
|
* Creates a typed `fetchNui` function for your event map.
|
|
110
130
|
* POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.
|
|
@@ -280,4 +300,4 @@ declare function _U(key: string, fallback: string, ...args: FormatArg[]): string
|
|
|
280
300
|
*/
|
|
281
301
|
declare function mergeLocales(...records: LocaleRecord[]): LocaleRecord;
|
|
282
302
|
|
|
283
|
-
export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, _U, createFetchNui, createTranslator, extendLocales, luaFormat, mergeLocales, onNuiMessage, registerLocales };
|
|
303
|
+
export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, _U, createFetchNui, createTranslator, extendLocales, isEnvBrowser, luaFormat, mergeLocales, onNuiMessage, registerLocales };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extend this to map your NUI
|
|
3
|
-
* Both `fetchNui` and `onNuiMessage` use this map for
|
|
2
|
+
* Extend this to map your NUI event names to their data/response shapes.
|
|
3
|
+
* Both `fetchNui` and `onNuiMessage` use this map for type inference.
|
|
4
4
|
*
|
|
5
5
|
* @example
|
|
6
6
|
* ```ts
|
|
@@ -59,6 +59,12 @@ interface FetchNuiFactoryOptions<TMap extends NuiEventMap> {
|
|
|
59
59
|
mockData?: {
|
|
60
60
|
[K in keyof TMap]?: TMap[K]["response"] | ((data: TMap[K]["data"]) => TMap[K]["response"]);
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* When `true`, mock data is ignored inside FiveM and real NUI callbacks are used instead.
|
|
64
|
+
* Mocks will still work in the browser for local development.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
disableMockInGame?: boolean;
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Flat strings or nested objects. Nested keys use dot-notation (`"ui.greeting"`).
|
|
@@ -105,6 +111,20 @@ type UnsubscribeFn = () => void;
|
|
|
105
111
|
/** Callback invoked when a matching NUI message arrives. */
|
|
106
112
|
type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
107
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Returns `true` when the UI is running inside a regular browser (outside FiveM).
|
|
116
|
+
* Checks for the absence of FiveM's `invokeNative` bridge on the `window` object.
|
|
117
|
+
*
|
|
118
|
+
* Use this to gate any logic that should only run inside the game client.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* if (isEnvBrowser()) {
|
|
123
|
+
* // Running in dev browser — skip game-only logic
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function isEnvBrowser(): boolean;
|
|
108
128
|
/**
|
|
109
129
|
* Creates a typed `fetchNui` function for your event map.
|
|
110
130
|
* POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.
|
|
@@ -280,4 +300,4 @@ declare function _U(key: string, fallback: string, ...args: FormatArg[]): string
|
|
|
280
300
|
*/
|
|
281
301
|
declare function mergeLocales(...records: LocaleRecord[]): LocaleRecord;
|
|
282
302
|
|
|
283
|
-
export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, _U, createFetchNui, createTranslator, extendLocales, luaFormat, mergeLocales, onNuiMessage, registerLocales };
|
|
303
|
+
export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, _U, createFetchNui, createTranslator, extendLocales, isEnvBrowser, luaFormat, mergeLocales, onNuiMessage, registerLocales };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
+
function isEnvBrowser() {
|
|
3
|
+
return typeof window !== "undefined" && !window.invokeNative;
|
|
4
|
+
}
|
|
2
5
|
function getResourceName() {
|
|
3
6
|
if (typeof window !== "undefined" && window.GetParentResourceName) {
|
|
4
7
|
return window.GetParentResourceName();
|
|
@@ -8,12 +11,14 @@ function getResourceName() {
|
|
|
8
11
|
function createFetchNui(factoryOptions) {
|
|
9
12
|
const debug = factoryOptions?.debug ?? false;
|
|
10
13
|
const mockData = factoryOptions?.mockData;
|
|
14
|
+
const disableMockInGame = factoryOptions?.disableMockInGame ?? false;
|
|
11
15
|
return async function fetchNui(event, ...args) {
|
|
12
16
|
const [data, options] = args;
|
|
13
17
|
if (debug) {
|
|
14
18
|
console.log(`[NUIX] \u2192 ${event}`, data ?? {});
|
|
15
19
|
}
|
|
16
|
-
|
|
20
|
+
const useMock = mockData && event in mockData && (isEnvBrowser() || !disableMockInGame);
|
|
21
|
+
if (useMock) {
|
|
17
22
|
const mock = mockData[event];
|
|
18
23
|
if (mock === void 0) {
|
|
19
24
|
throw new Error(`[NUIX] Mock data for "${event}" is undefined`);
|
|
@@ -24,6 +29,11 @@ function createFetchNui(factoryOptions) {
|
|
|
24
29
|
}
|
|
25
30
|
return result;
|
|
26
31
|
}
|
|
32
|
+
if (isEnvBrowser()) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`[NUIX] fetchNui("${event}") called in browser environment without mock data. Provide mockData in createFetchNui() options for local development.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
27
37
|
const url = `https://${getResourceName()}/${event}`;
|
|
28
38
|
const controller = options?.timeout ? new AbortController() : void 0;
|
|
29
39
|
let timeoutId;
|
|
@@ -150,6 +160,7 @@ export {
|
|
|
150
160
|
createFetchNui,
|
|
151
161
|
createTranslator,
|
|
152
162
|
extendLocales,
|
|
163
|
+
isEnvBrowser,
|
|
153
164
|
luaFormat,
|
|
154
165
|
mergeLocales,
|
|
155
166
|
onNuiMessage,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a typed `fetchNui` function for your event map.\r\n * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { data: { id: number }; response: { name: string } };\r\n * notify: { data: { msg: string }; response: void };\r\n * }\r\n *\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n *\r\n * // With mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (data) => { console.log(\"Mock:\", data.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\t...args: TMap[K][\"data\"] extends void\r\n\t\t\t? [data?: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tconst [data, options] = args;\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\r\n\t\tif (mockData && event in mockData) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (data: TMap[K][\"data\"]) => TMap[K][\"response\"])(data as TMap[K][\"data\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`).\r\n *\r\n * **Per-action** — filters by action name, `data` is fully typed:\r\n * ```ts\r\n * onNuiMessage<Events, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // ✅ typed as string[]\r\n * });\r\n * ```\r\n *\r\n * **Switch-case** — single listener for all actions:\r\n * ```ts\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * case \"showMenu\":\r\n * openMenu(data.items);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\n\r\n// Per-action overload\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"data\"]>,\r\n): UnsubscribeFn;\r\n\r\n// Switch-case overload\r\nexport function onNuiMessage<TMap extends NuiEventMap>(\r\n\thandler: (action: keyof TMap & string, data: any) => void,\r\n): UnsubscribeFn;\r\n\r\n// Implementation\r\nexport function onNuiMessage(\r\n\tactionOrHandler: string | ((action: string, data: unknown) => void),\r\n\thandler?: (data: unknown) => void,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (!payload.action) return;\r\n\r\n\t\tif (typeof actionOrHandler === \"string\") {\r\n\t\t\tif (payload.action !== actionOrHandler) return;\r\n\t\t\thandler!(payload.data);\r\n\t\t} else {\r\n\t\t\tactionOrHandler(payload.action, payload.data);\r\n\t\t}\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` / `%i` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates an isolated translator bound to a specific locale record.\r\n * Use this when you need a separate translator instance, independent of the global `_U`.\r\n *\r\n * @example\r\n * ```ts\r\n * const _T = createTranslator({\r\n * locales: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * });\r\n *\r\n * _T(\"greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _T(\"level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _T(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Global Locale Registry ───\r\n\r\nlet _locales: LocaleRecord = {};\r\n\r\n/**\r\n * Sets the global locale map. Call this when Lua sends locale data to the NUI.\r\n *\r\n * @example\r\n * ```ts\r\n * // Lua side:\r\n * // SendNUIMessage({ action = \"setLocales\", data = locales })\r\n *\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport function registerLocales(locales: LocaleRecord): void {\r\n\t_locales = locales;\r\n}\r\n\r\n/**\r\n * Merges new entries into the current global locale map without replacing it.\r\n *\r\n * @example\r\n * ```ts\r\n * registerLocales({ ui: { title: \"Dashboard\" } });\r\n * extendLocales({ ui: { subtitle: \"Overview\" } });\r\n *\r\n * _U(\"ui.title\", \"\"); // → \"Dashboard\"\r\n * _U(\"ui.subtitle\", \"\"); // → \"Overview\"\r\n * ```\r\n */\r\nexport function extendLocales(...records: LocaleRecord[]): void {\r\n\t_locales = mergeLocales(_locales, ...records);\r\n}\r\n\r\n/**\r\n * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.\r\n *\r\n * @param key Dot-notated key, e.g. `\"ui.greeting\"` or flat `\"title\"`\r\n * @param fallback Returned as-is when the key doesn't exist\r\n * @param args Values for `%s`, `%d`, `%f` placeholders\r\n *\r\n * @example\r\n * ```ts\r\n * import { registerLocales, _U } from \"@laot/nuix\";\r\n *\r\n * // After Lua sends locales:\r\n * // { ui: { greeting: \"Hello %s!\", level: \"Level %d\" } }\r\n *\r\n * _U(\"ui.greeting\", \"Hi\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"ui.level\", \"Lv.\", 42); // → \"Level 42\"\r\n * _U(\"missing.key\", \"Fallback\"); // → \"Fallback\"\r\n * ```\r\n */\r\nexport function _U(key: string, fallback: string, ...args: FormatArg[]): string {\r\n\tconst template = resolveKey(_locales, key);\r\n\tif (template === undefined) return fallback;\r\n\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (\r\n\t\t\t\tvalue !== null &&\r\n\t\t\t\texisting !== null &&\r\n\t\t\t\ttypeof value === \"object\" &&\r\n\t\t\t\ttypeof existing === \"object\" &&\r\n\t\t\t\texisting !== undefined\r\n\t\t\t) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Environment Detection ───\r\n\r\n/**\r\n * Returns `true` when the UI is running inside a regular browser (outside FiveM).\r\n * Checks for the absence of FiveM's `invokeNative` bridge on the `window` object.\r\n *\r\n * Use this to gate any logic that should only run inside the game client.\r\n *\r\n * @example\r\n * ```ts\r\n * if (isEnvBrowser()) {\r\n * // Running in dev browser — skip game-only logic\r\n * }\r\n * ```\r\n */\r\nexport function isEnvBrowser(): boolean {\r\n\treturn typeof window !== \"undefined\" && !(window as any).invokeNative;\r\n}\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a typed `fetchNui` function for your event map.\r\n * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { data: { id: number }; response: { name: string } };\r\n * notify: { data: { msg: string }; response: void };\r\n * }\r\n *\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n *\r\n * // With mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (data) => { console.log(\"Mock:\", data.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\tconst disableMockInGame = factoryOptions?.disableMockInGame ?? false;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\t...args: TMap[K][\"data\"] extends void\r\n\t\t\t? [data?: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"data\"], options?: FetchNuiOptions]\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tconst [data, options] = args;\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\t\t// Skip mocks inside FiveM when disableMockInGame is true\r\n\r\n\t\tconst useMock = mockData && event in mockData && (isEnvBrowser() || !disableMockInGame);\r\n\t\tif (useMock) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (data: TMap[K][\"data\"]) => TMap[K][\"response\"])(data as TMap[K][\"data\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Browser Guard ───\r\n\r\n\t\tif (isEnvBrowser()) {\r\n\t\t\tthrow new Error(\r\n\t\t\t\t`[NUIX] fetchNui(\"${event}\") called in browser environment without mock data. ` +\r\n\t\t\t\t\t`Provide mockData in createFetchNui() options for local development.`,\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`).\r\n *\r\n * **Per-action** — filters by action name, `data` is fully typed:\r\n * ```ts\r\n * onNuiMessage<Events, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // ✅ typed as string[]\r\n * });\r\n * ```\r\n *\r\n * **Switch-case** — single listener for all actions:\r\n * ```ts\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * case \"showMenu\":\r\n * openMenu(data.items);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\n\r\n// Per-action overload\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"data\"]>,\r\n): UnsubscribeFn;\r\n\r\n// Switch-case overload\r\nexport function onNuiMessage<TMap extends NuiEventMap>(\r\n\thandler: (action: keyof TMap & string, data: any) => void,\r\n): UnsubscribeFn;\r\n\r\n// Implementation\r\nexport function onNuiMessage(\r\n\tactionOrHandler: string | ((action: string, data: unknown) => void),\r\n\thandler?: (data: unknown) => void,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (!payload.action) return;\r\n\r\n\t\tif (typeof actionOrHandler === \"string\") {\r\n\t\t\tif (payload.action !== actionOrHandler) return;\r\n\t\t\thandler!(payload.data);\r\n\t\t} else {\r\n\t\t\tactionOrHandler(payload.action, payload.data);\r\n\t\t}\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` / `%i` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates an isolated translator bound to a specific locale record.\r\n * Use this when you need a separate translator instance, independent of the global `_U`.\r\n *\r\n * @example\r\n * ```ts\r\n * const _T = createTranslator({\r\n * locales: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * });\r\n *\r\n * _T(\"greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _T(\"level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _T(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Global Locale Registry ───\r\n\r\nlet _locales: LocaleRecord = {};\r\n\r\n/**\r\n * Sets the global locale map. Call this when Lua sends locale data to the NUI.\r\n *\r\n * @example\r\n * ```ts\r\n * // Lua side:\r\n * // SendNUIMessage({ action = \"setLocales\", data = locales })\r\n *\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport function registerLocales(locales: LocaleRecord): void {\r\n\t_locales = locales;\r\n}\r\n\r\n/**\r\n * Merges new entries into the current global locale map without replacing it.\r\n *\r\n * @example\r\n * ```ts\r\n * registerLocales({ ui: { title: \"Dashboard\" } });\r\n * extendLocales({ ui: { subtitle: \"Overview\" } });\r\n *\r\n * _U(\"ui.title\", \"\"); // → \"Dashboard\"\r\n * _U(\"ui.subtitle\", \"\"); // → \"Overview\"\r\n * ```\r\n */\r\nexport function extendLocales(...records: LocaleRecord[]): void {\r\n\t_locales = mergeLocales(_locales, ...records);\r\n}\r\n\r\n/**\r\n * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.\r\n *\r\n * @param key Dot-notated key, e.g. `\"ui.greeting\"` or flat `\"title\"`\r\n * @param fallback Returned as-is when the key doesn't exist\r\n * @param args Values for `%s`, `%d`, `%f` placeholders\r\n *\r\n * @example\r\n * ```ts\r\n * import { registerLocales, _U } from \"@laot/nuix\";\r\n *\r\n * // After Lua sends locales:\r\n * // { ui: { greeting: \"Hello %s!\", level: \"Level %d\" } }\r\n *\r\n * _U(\"ui.greeting\", \"Hi\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"ui.level\", \"Lv.\", 42); // → \"Level 42\"\r\n * _U(\"missing.key\", \"Fallback\"); // → \"Fallback\"\r\n * ```\r\n */\r\nexport function _U(key: string, fallback: string, ...args: FormatArg[]): string {\r\n\tconst template = resolveKey(_locales, key);\r\n\tif (template === undefined) return fallback;\r\n\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (\r\n\t\t\t\tvalue !== null &&\r\n\t\t\t\texisting !== null &&\r\n\t\t\t\ttypeof value === \"object\" &&\r\n\t\t\t\ttypeof existing === \"object\" &&\r\n\t\t\t\texisting !== undefined\r\n\t\t\t) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
|