@laot/nuix 1.0.0 → 1.0.2
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 +98 -48
- package/dist/index.cjs +31 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -60
- package/dist/index.d.ts +95 -60
- package/dist/index.js +27 -6
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# NUIX
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@laot/nuix)
|
|
4
|
+
[](https://github.com/laot7490/nuix/blob/main/LICENSE)
|
|
5
|
+
[](https://bundlephobia.com/package/@laot/nuix)
|
|
6
|
+
|
|
3
7
|
> Modular, type-safe TypeScript library for FiveM NUI projects. Zero runtime dependencies.
|
|
4
8
|
|
|
5
9
|
## Install
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
npm install @laot/nuix
|
|
13
|
+
pnpm add @laot/nuix
|
|
14
|
+
yarn add @laot/nuix
|
|
15
|
+
bun add @laot/nuix
|
|
9
16
|
```
|
|
10
17
|
|
|
11
18
|
## Quick Start
|
|
@@ -16,8 +23,9 @@ npm install @laot/nuix
|
|
|
16
23
|
import type { NuiEventMap } from "@laot/nuix";
|
|
17
24
|
|
|
18
25
|
interface MyEvents extends NuiEventMap {
|
|
19
|
-
getPlayer:
|
|
26
|
+
getPlayer: { request: { id: number }; response: { name: string; level: number } };
|
|
20
27
|
sendNotify: { request: { message: string }; response: void };
|
|
28
|
+
showMenu: { request: { items: string[] }; response: void };
|
|
21
29
|
}
|
|
22
30
|
```
|
|
23
31
|
|
|
@@ -28,9 +36,8 @@ import { createFetchNui } from "@laot/nuix";
|
|
|
28
36
|
|
|
29
37
|
const fetchNui = createFetchNui<MyEvents>();
|
|
30
38
|
|
|
31
|
-
// Fully typed — request and response inferred from event map
|
|
32
39
|
const player = await fetchNui("getPlayer", { id: 1 });
|
|
33
|
-
console.log(player.name); //
|
|
40
|
+
console.log(player.name); // string
|
|
34
41
|
|
|
35
42
|
await fetchNui("sendNotify", { message: "Hello!" });
|
|
36
43
|
|
|
@@ -40,20 +47,31 @@ const data = await fetchNui("getPlayer", { id: 2 }, { timeout: 5000 });
|
|
|
40
47
|
|
|
41
48
|
### 3. NUI Message Listener
|
|
42
49
|
|
|
50
|
+
**Switch-case** — single listener for all actions:
|
|
51
|
+
|
|
43
52
|
```ts
|
|
44
53
|
import { onNuiMessage } from "@laot/nuix";
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
const unsub = onNuiMessage<MyEvents>((action, data) => {
|
|
56
|
+
switch (action) {
|
|
57
|
+
case "getPlayer":
|
|
58
|
+
console.log(data.name);
|
|
59
|
+
break;
|
|
60
|
+
case "sendNotify":
|
|
61
|
+
console.log(data.message);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
50
65
|
|
|
51
|
-
|
|
66
|
+
unsub(); // stop listening
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Per-action** — filtered by action, `data` is fully typed:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const unsub = onNuiMessage<MyEvents, "showMenu">("showMenu", (data) => {
|
|
52
73
|
console.log(data.items); // ✅ typed as string[]
|
|
53
74
|
});
|
|
54
|
-
|
|
55
|
-
// Clean up
|
|
56
|
-
unsub();
|
|
57
75
|
```
|
|
58
76
|
|
|
59
77
|
### 4. Lua-Style Formatter
|
|
@@ -68,63 +86,89 @@ luaFormat("Accuracy: %f%%", 99.5);
|
|
|
68
86
|
// → "Accuracy: 99.5%"
|
|
69
87
|
```
|
|
70
88
|
|
|
71
|
-
### 5. Translator
|
|
89
|
+
### 5. Translator (Global)
|
|
90
|
+
|
|
91
|
+
Register locales once at runtime (e.g. when Lua sends them), then use `_U` anywhere:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { registerLocales, _U, onNuiMessage } from "@laot/nuix";
|
|
95
|
+
import type { NuiEventMap, LocaleRecord } from "@laot/nuix";
|
|
96
|
+
|
|
97
|
+
interface Events extends NuiEventMap {
|
|
98
|
+
setLocales: { request: LocaleRecord; response: void };
|
|
99
|
+
showMenu: { request: { items: string[] }; response: void };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onNuiMessage<Events>((action, data) => {
|
|
103
|
+
switch (action) {
|
|
104
|
+
case "setLocales":
|
|
105
|
+
registerLocales(data);
|
|
106
|
+
break;
|
|
107
|
+
case "showMenu":
|
|
108
|
+
openMenu(data.items);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Use _U anywhere
|
|
114
|
+
_U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
|
|
115
|
+
_U("ui.level", "Lv.", 42); // → "Level 42"
|
|
116
|
+
_U("missing.key", "Fallback"); // → "Fallback"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You can also extend locales incrementally with `extendLocales`:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { extendLocales } from "@laot/nuix";
|
|
123
|
+
|
|
124
|
+
extendLocales({ ui: { subtitle: "Overview" } });
|
|
125
|
+
// Merges into existing locales without replacing them
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 6. Translator (Isolated)
|
|
129
|
+
|
|
130
|
+
If you need a separate translator instance with its own locale scope:
|
|
72
131
|
|
|
73
132
|
```ts
|
|
74
133
|
import { createTranslator, mergeLocales } from "@laot/nuix";
|
|
75
134
|
|
|
76
|
-
const
|
|
135
|
+
const _T = createTranslator({
|
|
77
136
|
locales: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
level: "Level %d",
|
|
81
|
-
},
|
|
82
|
-
server: {
|
|
83
|
-
error: "Error: %s",
|
|
84
|
-
},
|
|
85
|
-
flat_key: "Plain message: %s",
|
|
137
|
+
greeting: "Hello %s!",
|
|
138
|
+
level: "Level %d",
|
|
86
139
|
},
|
|
87
140
|
});
|
|
88
141
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
_U("flat_key", "MISSING", "test"); // → "Plain message: test"
|
|
92
|
-
_U("no.key", "Not found"); // → "Not found"
|
|
93
|
-
|
|
94
|
-
// Deep-merge multiple locale files
|
|
95
|
-
const base = { client: { greeting: "Hello %s!" } };
|
|
96
|
-
const overrides = { client: { greeting: "Hey %s, welcome back!" } };
|
|
97
|
-
const merged = mergeLocales(base, overrides);
|
|
98
|
-
const _T = createTranslator({ locales: merged });
|
|
142
|
+
_T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
|
|
143
|
+
_T("level", "MISSING", 42); // → "Level 42"
|
|
99
144
|
|
|
100
|
-
|
|
145
|
+
// Deep-merge multiple locale records
|
|
146
|
+
const base = { ui: { greeting: "Hello %s!" } };
|
|
147
|
+
const patch = { ui: { greeting: "Hey %s, welcome back!" } };
|
|
148
|
+
const merged = mergeLocales(base, patch);
|
|
101
149
|
```
|
|
102
150
|
|
|
103
|
-
###
|
|
151
|
+
### 7. Debug Mode
|
|
104
152
|
|
|
105
|
-
Enable console logging for every `fetchNui` call
|
|
153
|
+
Enable console logging for every `fetchNui` call:
|
|
106
154
|
|
|
107
155
|
```ts
|
|
108
156
|
const fetchNui = createFetchNui<MyEvents>({ debug: true });
|
|
109
157
|
|
|
110
158
|
await fetchNui("getPlayer", { id: 1 });
|
|
111
|
-
// Console:
|
|
112
159
|
// [NUIX] → getPlayer { id: 1 }
|
|
113
160
|
// [NUIX] ← getPlayer { name: "Laot", level: 42 }
|
|
114
161
|
```
|
|
115
162
|
|
|
116
|
-
###
|
|
163
|
+
### 8. Mock Data (Local Development)
|
|
117
164
|
|
|
118
|
-
|
|
165
|
+
Return pre-defined responses without real HTTP calls — useful when developing outside FiveM:
|
|
119
166
|
|
|
120
167
|
```ts
|
|
121
168
|
const fetchNui = createFetchNui<MyEvents>({
|
|
122
169
|
debug: true,
|
|
123
170
|
mockData: {
|
|
124
|
-
// Static response
|
|
125
171
|
getPlayer: { name: "DevPlayer", level: 99 },
|
|
126
|
-
|
|
127
|
-
// Dynamic response based on request
|
|
128
172
|
sendNotify: (req) => {
|
|
129
173
|
console.log("Mock notification:", req.message);
|
|
130
174
|
},
|
|
@@ -132,22 +176,24 @@ const fetchNui = createFetchNui<MyEvents>({
|
|
|
132
176
|
});
|
|
133
177
|
|
|
134
178
|
const player = await fetchNui("getPlayer", { id: 1 });
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// player = { name: "DevPlayer", level: 99 }
|
|
179
|
+
// [NUIX] → getPlayer { id: 1 }
|
|
180
|
+
// [NUIX] ← getPlayer (mock) { name: "DevPlayer", level: 99 }
|
|
138
181
|
```
|
|
139
182
|
|
|
140
|
-
## Lua
|
|
183
|
+
## Lua Examples
|
|
141
184
|
|
|
142
185
|
```lua
|
|
143
|
-
--
|
|
186
|
+
-- NUI callback (responds to fetchNui calls)
|
|
144
187
|
RegisterNUICallback("getPlayer", function(data, cb)
|
|
145
188
|
local player = GetPlayerData(data.id)
|
|
146
189
|
cb({ name = player.name, level = player.level })
|
|
147
190
|
end)
|
|
148
191
|
|
|
149
|
-
--
|
|
192
|
+
-- Send messages to NUI
|
|
150
193
|
SendNUIMessage({ action = "showMenu", data = { items = {"Pistol", "Rifle"} } })
|
|
194
|
+
|
|
195
|
+
-- Send locales to NUI (for registerLocales)
|
|
196
|
+
SendNUIMessage({ action = "setLocales", data = Locales })
|
|
151
197
|
```
|
|
152
198
|
|
|
153
199
|
## API Reference
|
|
@@ -155,9 +201,13 @@ SendNUIMessage({ action = "showMenu", data = { items = {"Pistol", "Rifle"} } })
|
|
|
155
201
|
| Export | Type | Description |
|
|
156
202
|
|---|---|---|
|
|
157
203
|
| `createFetchNui<TMap>(options?)` | Factory | Returns a typed `fetchNui` function (supports debug & mock) |
|
|
158
|
-
| `onNuiMessage<TMap
|
|
204
|
+
| `onNuiMessage<TMap>(handler)` | Function | Single listener for all actions (switch-case) |
|
|
205
|
+
| `onNuiMessage<TMap, K>(action, handler)` | Function | Per-action listener with typed data |
|
|
159
206
|
| `luaFormat(template, ...args)` | Function | Lua-style `%s`/`%d`/`%f` formatter |
|
|
160
|
-
| `
|
|
207
|
+
| `registerLocales(locales)` | Function | Sets the global locale map at runtime |
|
|
208
|
+
| `extendLocales(...records)` | Function | Merges new entries into the global locale map |
|
|
209
|
+
| `_U(key, fallback, ...args)` | Function | Global translator — reads from registered locales |
|
|
210
|
+
| `createTranslator(options)` | Factory | Returns an isolated translator function |
|
|
161
211
|
| `mergeLocales(...records)` | Function | Deep-merges locale records |
|
|
162
212
|
|
|
163
213
|
## Build
|
package/dist/index.cjs
CHANGED
|
@@ -20,11 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
_U: () => _U,
|
|
23
24
|
createFetchNui: () => createFetchNui,
|
|
24
25
|
createTranslator: () => createTranslator,
|
|
26
|
+
extendLocales: () => extendLocales,
|
|
25
27
|
luaFormat: () => luaFormat,
|
|
26
28
|
mergeLocales: () => mergeLocales,
|
|
27
|
-
onNuiMessage: () => onNuiMessage
|
|
29
|
+
onNuiMessage: () => onNuiMessage,
|
|
30
|
+
registerLocales: () => registerLocales
|
|
28
31
|
});
|
|
29
32
|
module.exports = __toCommonJS(index_exports);
|
|
30
33
|
|
|
@@ -38,7 +41,8 @@ function getResourceName() {
|
|
|
38
41
|
function createFetchNui(factoryOptions) {
|
|
39
42
|
const debug = factoryOptions?.debug ?? false;
|
|
40
43
|
const mockData = factoryOptions?.mockData;
|
|
41
|
-
return async function fetchNui(event,
|
|
44
|
+
return async function fetchNui(event, ...args) {
|
|
45
|
+
const [data, options] = args;
|
|
42
46
|
if (debug) {
|
|
43
47
|
console.log(`[NUIX] \u2192 ${event}`, data ?? {});
|
|
44
48
|
}
|
|
@@ -86,12 +90,17 @@ function createFetchNui(factoryOptions) {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
// src/listener.ts
|
|
89
|
-
function onNuiMessage(
|
|
93
|
+
function onNuiMessage(actionOrHandler, handler) {
|
|
90
94
|
const listener = (event) => {
|
|
91
95
|
const payload = event.data;
|
|
92
96
|
if (!payload || typeof payload !== "object") return;
|
|
93
|
-
if (payload.action
|
|
94
|
-
|
|
97
|
+
if (!payload.action) return;
|
|
98
|
+
if (typeof actionOrHandler === "string") {
|
|
99
|
+
if (payload.action !== actionOrHandler) return;
|
|
100
|
+
handler(payload.data);
|
|
101
|
+
} else {
|
|
102
|
+
actionOrHandler(payload.action, payload.data);
|
|
103
|
+
}
|
|
95
104
|
};
|
|
96
105
|
window.addEventListener("message", listener);
|
|
97
106
|
return () => {
|
|
@@ -143,12 +152,24 @@ function createTranslator(options) {
|
|
|
143
152
|
return args.length > 0 ? luaFormat(template, ...args) : template;
|
|
144
153
|
};
|
|
145
154
|
}
|
|
155
|
+
var _locales = {};
|
|
156
|
+
function registerLocales(locales) {
|
|
157
|
+
_locales = locales;
|
|
158
|
+
}
|
|
159
|
+
function extendLocales(...records) {
|
|
160
|
+
_locales = mergeLocales(_locales, ...records);
|
|
161
|
+
}
|
|
162
|
+
function _U(key, fallback, ...args) {
|
|
163
|
+
const template = resolveKey(_locales, key);
|
|
164
|
+
if (template === void 0) return fallback;
|
|
165
|
+
return args.length > 0 ? luaFormat(template, ...args) : template;
|
|
166
|
+
}
|
|
146
167
|
function mergeLocales(...records) {
|
|
147
168
|
const result = {};
|
|
148
169
|
for (const record of records) {
|
|
149
170
|
for (const [key, value] of Object.entries(record)) {
|
|
150
171
|
const existing = result[key];
|
|
151
|
-
if (typeof value === "object" && typeof existing === "object" && existing !== void 0) {
|
|
172
|
+
if (value !== null && existing !== null && typeof value === "object" && typeof existing === "object" && existing !== void 0) {
|
|
152
173
|
result[key] = mergeLocales(existing, value);
|
|
153
174
|
} else {
|
|
154
175
|
result[key] = value;
|
|
@@ -159,10 +180,13 @@ function mergeLocales(...records) {
|
|
|
159
180
|
}
|
|
160
181
|
// Annotate the CommonJS export names for ESM import in node:
|
|
161
182
|
0 && (module.exports = {
|
|
183
|
+
_U,
|
|
162
184
|
createFetchNui,
|
|
163
185
|
createTranslator,
|
|
186
|
+
extendLocales,
|
|
164
187
|
luaFormat,
|
|
165
188
|
mergeLocales,
|
|
166
|
-
onNuiMessage
|
|
189
|
+
onNuiMessage,
|
|
190
|
+
registerLocales
|
|
167
191
|
});
|
|
168
192
|
//# sourceMappingURL=index.cjs.map
|
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 } 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 fully typed `fetchNui` function tied to your event map.\r\n *\r\n * The returned function POSTs JSON to `https://<resourceName>/<event>`,\r\n * which maps 1:1 to `RegisterNUICallback` on the Lua side.\r\n *\r\n * Supports optional debug logging, mock data for local dev, and per-call timeout.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { msg: string }; response: void };\r\n * }\r\n *\r\n * // Production usage\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n * // player.name is typed as string\r\n *\r\n * // Local dev with mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (req) => { console.log(\"Mock:\", req.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\tdata?: TMap[K][\"request\"],\r\n\t\toptions?: FetchNuiOptions,\r\n\t): Promise<TMap[K][\"response\"]> {\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 (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\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`), filtered by action name.\r\n * Only messages matching the given `action` trigger the handler.\r\n * Returns a cleanup function to stop listening.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyMessages extends NuiEventMap {\r\n * showMenu: { request: { items: string[] }; response: void };\r\n * hideMenu: { request: void; response: void };\r\n * }\r\n *\r\n * const unsub = onNuiMessage<MyMessages, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // typed as string[]\r\n * });\r\n *\r\n * // Stop listening when done\r\n * unsub();\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\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"request\"]>,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload<TMap[K][\"request\"]>>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (payload.action !== action) return;\r\n\r\n\t\thandler(payload.data);\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` — 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 a `_U(key, fallback, ...args)` translator bound to a locale record.\r\n *\r\n * The key supports dot-notation to traverse nested locale objects.\r\n * If the key isn't found, the fallback string is returned as-is.\r\n * Any extra args are passed through `luaFormat` for placeholder substitution.\r\n *\r\n * @example\r\n * ```ts\r\n * const _U = createTranslator({\r\n * locales: {\r\n * client: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * server: {\r\n * error: \"Error: %s\",\r\n * },\r\n * flat_key: \"Plain message\",\r\n * },\r\n * });\r\n *\r\n * _U(\"client.greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"client.level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _U(\"flat_key\", \"MISSING\"); // → \"Plain message\"\r\n * _U(\"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// ─── 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 (typeof value === \"object\" && typeof existing === \"object\" && existing !== undefined) {\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 } 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: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { 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: (req) => { console.log(\"Mock:\", req.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][\"request\"] extends void\r\n\t\t\t? [data?: TMap[K][\"request\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"request\"], 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 (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\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][\"request\"]>,\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
|
@@ -39,7 +39,7 @@ interface FetchNuiOptions {
|
|
|
39
39
|
timeout?: number;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* Config for `createFetchNui()`.
|
|
43
43
|
*
|
|
44
44
|
* @example
|
|
45
45
|
* ```ts
|
|
@@ -53,25 +53,20 @@ interface FetchNuiOptions {
|
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
55
|
interface FetchNuiFactoryOptions<TMap extends NuiEventMap> {
|
|
56
|
-
/** Logs every
|
|
56
|
+
/** Logs every call and response to the console with `[NUIX]` prefix. */
|
|
57
57
|
debug?: boolean;
|
|
58
|
-
/**
|
|
59
|
-
* Mock responses for local development outside FiveM.
|
|
60
|
-
* Can be a static value or a function that receives the request and returns the response.
|
|
61
|
-
* When a mock exists for an event, no HTTP call is made.
|
|
62
|
-
*/
|
|
58
|
+
/** Static or dynamic mock responses — when set, no real HTTP call is made. */
|
|
63
59
|
mockData?: {
|
|
64
60
|
[K in keyof TMap]?: TMap[K]["response"] | ((request: TMap[K]["request"]) => TMap[K]["response"]);
|
|
65
61
|
};
|
|
66
62
|
}
|
|
67
63
|
/**
|
|
68
|
-
*
|
|
69
|
-
* Nested keys are accessed via dot-notation in the translator.
|
|
64
|
+
* Flat strings or nested objects. Nested keys use dot-notation (`"ui.greeting"`).
|
|
70
65
|
*
|
|
71
66
|
* @example
|
|
72
67
|
* ```ts
|
|
73
68
|
* const locales: LocaleRecord = {
|
|
74
|
-
*
|
|
69
|
+
* ui: {
|
|
75
70
|
* greeting: "Hello %s!",
|
|
76
71
|
* level: "Level %d",
|
|
77
72
|
* },
|
|
@@ -83,20 +78,20 @@ type LocaleRecord = {
|
|
|
83
78
|
[key: string]: string | LocaleRecord;
|
|
84
79
|
};
|
|
85
80
|
interface TranslatorOptions {
|
|
86
|
-
/** The locale map
|
|
81
|
+
/** The locale map — flat or nested. */
|
|
87
82
|
locales: LocaleRecord;
|
|
88
83
|
}
|
|
89
84
|
/**
|
|
90
|
-
* Translator function
|
|
85
|
+
* Translator function signature used by both `createTranslator` and the global `_U`.
|
|
91
86
|
*
|
|
92
|
-
* @param key
|
|
93
|
-
* @param fallback
|
|
94
|
-
* @param args
|
|
87
|
+
* @param key Dot-notated key like `"ui.greeting"` or flat like `"title"`
|
|
88
|
+
* @param fallback Returned when the key doesn't exist in the locale map
|
|
89
|
+
* @param args Format arguments for `%s`, `%d`, `%f` placeholders
|
|
95
90
|
*
|
|
96
91
|
* @example
|
|
97
92
|
* ```ts
|
|
98
|
-
* _U("
|
|
99
|
-
* _U("
|
|
93
|
+
* _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
|
|
94
|
+
* _U("missing.key", "Fallback"); // → "Fallback"
|
|
100
95
|
* ```
|
|
101
96
|
*/
|
|
102
97
|
type TranslatorFn = (key: string, fallback: string, ...args: FormatArg[]) => string;
|
|
@@ -111,12 +106,8 @@ type UnsubscribeFn = () => void;
|
|
|
111
106
|
type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
112
107
|
|
|
113
108
|
/**
|
|
114
|
-
* Creates a
|
|
115
|
-
*
|
|
116
|
-
* The returned function POSTs JSON to `https://<resourceName>/<event>`,
|
|
117
|
-
* which maps 1:1 to `RegisterNUICallback` on the Lua side.
|
|
118
|
-
*
|
|
119
|
-
* Supports optional debug logging, mock data for local dev, and per-call timeout.
|
|
109
|
+
* Creates a typed `fetchNui` function for your event map.
|
|
110
|
+
* POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.
|
|
120
111
|
*
|
|
121
112
|
* @example
|
|
122
113
|
* ```ts
|
|
@@ -125,12 +116,10 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
|
125
116
|
* notify: { request: { msg: string }; response: void };
|
|
126
117
|
* }
|
|
127
118
|
*
|
|
128
|
-
* // Production usage
|
|
129
119
|
* const fetchNui = createFetchNui<MyEvents>();
|
|
130
120
|
* const player = await fetchNui("getPlayer", { id: 1 });
|
|
131
|
-
* // player.name is typed as string
|
|
132
121
|
*
|
|
133
|
-
* //
|
|
122
|
+
* // With mocks + debug
|
|
134
123
|
* const fetchNui = createFetchNui<MyEvents>({
|
|
135
124
|
* debug: true,
|
|
136
125
|
* mockData: {
|
|
@@ -148,26 +137,30 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
|
148
137
|
* end)
|
|
149
138
|
* ```
|
|
150
139
|
*/
|
|
151
|
-
declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, data?: TMap[K]["request"], options?: FetchNuiOptions) => Promise<TMap[K]["response"]>;
|
|
140
|
+
declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, ...args: TMap[K]["request"] extends void ? [data?: TMap[K]["request"], options?: FetchNuiOptions] : [data: TMap[K]["request"], options?: FetchNuiOptions]) => Promise<TMap[K]["response"]>;
|
|
152
141
|
|
|
153
142
|
/**
|
|
154
|
-
* Listens for NUI messages from Lua (`SendNUIMessage`)
|
|
155
|
-
* Only messages matching the given `action` trigger the handler.
|
|
156
|
-
* Returns a cleanup function to stop listening.
|
|
143
|
+
* Listens for NUI messages from Lua (`SendNUIMessage`).
|
|
157
144
|
*
|
|
158
|
-
*
|
|
145
|
+
* **Per-action** — filters by action name, `data` is fully typed:
|
|
159
146
|
* ```ts
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* hideMenu: { request: void; response: void };
|
|
163
|
-
* }
|
|
164
|
-
*
|
|
165
|
-
* const unsub = onNuiMessage<MyMessages, "showMenu">("showMenu", (data) => {
|
|
166
|
-
* console.log(data.items); // typed as string[]
|
|
147
|
+
* onNuiMessage<Events, "showMenu">("showMenu", (data) => {
|
|
148
|
+
* console.log(data.items); // ✅ typed as string[]
|
|
167
149
|
* });
|
|
150
|
+
* ```
|
|
168
151
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
152
|
+
* **Switch-case** — single listener for all actions:
|
|
153
|
+
* ```ts
|
|
154
|
+
* onNuiMessage<Events>((action, data) => {
|
|
155
|
+
* switch (action) {
|
|
156
|
+
* case "setLocales":
|
|
157
|
+
* registerLocales(data);
|
|
158
|
+
* break;
|
|
159
|
+
* case "showMenu":
|
|
160
|
+
* openMenu(data.items);
|
|
161
|
+
* break;
|
|
162
|
+
* }
|
|
163
|
+
* });
|
|
171
164
|
* ```
|
|
172
165
|
*
|
|
173
166
|
* Lua side:
|
|
@@ -176,13 +169,14 @@ declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: Fetch
|
|
|
176
169
|
* ```
|
|
177
170
|
*/
|
|
178
171
|
declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(action: K, handler: NuiMessageHandler<TMap[K]["request"]>): UnsubscribeFn;
|
|
172
|
+
declare function onNuiMessage<TMap extends NuiEventMap>(handler: (action: keyof TMap & string, data: any) => void): UnsubscribeFn;
|
|
179
173
|
|
|
180
174
|
/**
|
|
181
175
|
* Formats a string using Lua-style placeholders.
|
|
182
176
|
*
|
|
183
177
|
* Specifiers:
|
|
184
178
|
* - `%s` — string (null/undefined becomes empty string)
|
|
185
|
-
* - `%d` — integer (floors the value, NaN becomes 0)
|
|
179
|
+
* - `%d` / `%i` — integer (floors the value, NaN becomes 0)
|
|
186
180
|
* - `%f` — float (NaN becomes 0)
|
|
187
181
|
* - `%%` — literal percent sign
|
|
188
182
|
*
|
|
@@ -200,34 +194,75 @@ declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & s
|
|
|
200
194
|
*/
|
|
201
195
|
declare function luaFormat(template: string, ...args: FormatArg[]): string;
|
|
202
196
|
/**
|
|
203
|
-
* Creates
|
|
204
|
-
*
|
|
205
|
-
* The key supports dot-notation to traverse nested locale objects.
|
|
206
|
-
* If the key isn't found, the fallback string is returned as-is.
|
|
207
|
-
* Any extra args are passed through `luaFormat` for placeholder substitution.
|
|
197
|
+
* Creates an isolated translator bound to a specific locale record.
|
|
198
|
+
* Use this when you need a separate translator instance, independent of the global `_U`.
|
|
208
199
|
*
|
|
209
200
|
* @example
|
|
210
201
|
* ```ts
|
|
211
|
-
* const
|
|
202
|
+
* const _T = createTranslator({
|
|
212
203
|
* locales: {
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* level: "Level %d",
|
|
216
|
-
* },
|
|
217
|
-
* server: {
|
|
218
|
-
* error: "Error: %s",
|
|
219
|
-
* },
|
|
220
|
-
* flat_key: "Plain message",
|
|
204
|
+
* greeting: "Hello %s!",
|
|
205
|
+
* level: "Level %d",
|
|
221
206
|
* },
|
|
222
207
|
* });
|
|
223
208
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* _U("no.key", "Not found"); // → "Not found"
|
|
209
|
+
* _T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
|
|
210
|
+
* _T("level", "MISSING", 42); // → "Level 42"
|
|
211
|
+
* _T("no.key", "Not found"); // → "Not found"
|
|
228
212
|
* ```
|
|
229
213
|
*/
|
|
230
214
|
declare function createTranslator(options: TranslatorOptions): TranslatorFn;
|
|
215
|
+
/**
|
|
216
|
+
* Sets the global locale map. Call this when Lua sends locale data to the NUI.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* // Lua side:
|
|
221
|
+
* // SendNUIMessage({ action = "setLocales", data = locales })
|
|
222
|
+
*
|
|
223
|
+
* onNuiMessage<Events>((action, data) => {
|
|
224
|
+
* switch (action) {
|
|
225
|
+
* case "setLocales":
|
|
226
|
+
* registerLocales(data);
|
|
227
|
+
* break;
|
|
228
|
+
* }
|
|
229
|
+
* });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare function registerLocales(locales: LocaleRecord): void;
|
|
233
|
+
/**
|
|
234
|
+
* Merges new entries into the current global locale map without replacing it.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* registerLocales({ ui: { title: "Dashboard" } });
|
|
239
|
+
* extendLocales({ ui: { subtitle: "Overview" } });
|
|
240
|
+
*
|
|
241
|
+
* _U("ui.title", ""); // → "Dashboard"
|
|
242
|
+
* _U("ui.subtitle", ""); // → "Overview"
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
declare function extendLocales(...records: LocaleRecord[]): void;
|
|
246
|
+
/**
|
|
247
|
+
* Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.
|
|
248
|
+
*
|
|
249
|
+
* @param key Dot-notated key, e.g. `"ui.greeting"` or flat `"title"`
|
|
250
|
+
* @param fallback Returned as-is when the key doesn't exist
|
|
251
|
+
* @param args Values for `%s`, `%d`, `%f` placeholders
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```ts
|
|
255
|
+
* import { registerLocales, _U } from "@laot/nuix";
|
|
256
|
+
*
|
|
257
|
+
* // After Lua sends locales:
|
|
258
|
+
* // { ui: { greeting: "Hello %s!", level: "Level %d" } }
|
|
259
|
+
*
|
|
260
|
+
* _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
|
|
261
|
+
* _U("ui.level", "Lv.", 42); // → "Level 42"
|
|
262
|
+
* _U("missing.key", "Fallback"); // → "Fallback"
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
declare function _U(key: string, fallback: string, ...args: FormatArg[]): string;
|
|
231
266
|
/**
|
|
232
267
|
* Deep-merges multiple locale records into one.
|
|
233
268
|
* Later records override earlier ones on key conflicts.
|
|
@@ -245,4 +280,4 @@ declare function createTranslator(options: TranslatorOptions): TranslatorFn;
|
|
|
245
280
|
*/
|
|
246
281
|
declare function mergeLocales(...records: LocaleRecord[]): LocaleRecord;
|
|
247
282
|
|
|
248
|
-
export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, createFetchNui, createTranslator, luaFormat, mergeLocales, onNuiMessage };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface FetchNuiOptions {
|
|
|
39
39
|
timeout?: number;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* Config for `createFetchNui()`.
|
|
43
43
|
*
|
|
44
44
|
* @example
|
|
45
45
|
* ```ts
|
|
@@ -53,25 +53,20 @@ interface FetchNuiOptions {
|
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
55
|
interface FetchNuiFactoryOptions<TMap extends NuiEventMap> {
|
|
56
|
-
/** Logs every
|
|
56
|
+
/** Logs every call and response to the console with `[NUIX]` prefix. */
|
|
57
57
|
debug?: boolean;
|
|
58
|
-
/**
|
|
59
|
-
* Mock responses for local development outside FiveM.
|
|
60
|
-
* Can be a static value or a function that receives the request and returns the response.
|
|
61
|
-
* When a mock exists for an event, no HTTP call is made.
|
|
62
|
-
*/
|
|
58
|
+
/** Static or dynamic mock responses — when set, no real HTTP call is made. */
|
|
63
59
|
mockData?: {
|
|
64
60
|
[K in keyof TMap]?: TMap[K]["response"] | ((request: TMap[K]["request"]) => TMap[K]["response"]);
|
|
65
61
|
};
|
|
66
62
|
}
|
|
67
63
|
/**
|
|
68
|
-
*
|
|
69
|
-
* Nested keys are accessed via dot-notation in the translator.
|
|
64
|
+
* Flat strings or nested objects. Nested keys use dot-notation (`"ui.greeting"`).
|
|
70
65
|
*
|
|
71
66
|
* @example
|
|
72
67
|
* ```ts
|
|
73
68
|
* const locales: LocaleRecord = {
|
|
74
|
-
*
|
|
69
|
+
* ui: {
|
|
75
70
|
* greeting: "Hello %s!",
|
|
76
71
|
* level: "Level %d",
|
|
77
72
|
* },
|
|
@@ -83,20 +78,20 @@ type LocaleRecord = {
|
|
|
83
78
|
[key: string]: string | LocaleRecord;
|
|
84
79
|
};
|
|
85
80
|
interface TranslatorOptions {
|
|
86
|
-
/** The locale map
|
|
81
|
+
/** The locale map — flat or nested. */
|
|
87
82
|
locales: LocaleRecord;
|
|
88
83
|
}
|
|
89
84
|
/**
|
|
90
|
-
* Translator function
|
|
85
|
+
* Translator function signature used by both `createTranslator` and the global `_U`.
|
|
91
86
|
*
|
|
92
|
-
* @param key
|
|
93
|
-
* @param fallback
|
|
94
|
-
* @param args
|
|
87
|
+
* @param key Dot-notated key like `"ui.greeting"` or flat like `"title"`
|
|
88
|
+
* @param fallback Returned when the key doesn't exist in the locale map
|
|
89
|
+
* @param args Format arguments for `%s`, `%d`, `%f` placeholders
|
|
95
90
|
*
|
|
96
91
|
* @example
|
|
97
92
|
* ```ts
|
|
98
|
-
* _U("
|
|
99
|
-
* _U("
|
|
93
|
+
* _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
|
|
94
|
+
* _U("missing.key", "Fallback"); // → "Fallback"
|
|
100
95
|
* ```
|
|
101
96
|
*/
|
|
102
97
|
type TranslatorFn = (key: string, fallback: string, ...args: FormatArg[]) => string;
|
|
@@ -111,12 +106,8 @@ type UnsubscribeFn = () => void;
|
|
|
111
106
|
type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
112
107
|
|
|
113
108
|
/**
|
|
114
|
-
* Creates a
|
|
115
|
-
*
|
|
116
|
-
* The returned function POSTs JSON to `https://<resourceName>/<event>`,
|
|
117
|
-
* which maps 1:1 to `RegisterNUICallback` on the Lua side.
|
|
118
|
-
*
|
|
119
|
-
* Supports optional debug logging, mock data for local dev, and per-call timeout.
|
|
109
|
+
* Creates a typed `fetchNui` function for your event map.
|
|
110
|
+
* POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.
|
|
120
111
|
*
|
|
121
112
|
* @example
|
|
122
113
|
* ```ts
|
|
@@ -125,12 +116,10 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
|
125
116
|
* notify: { request: { msg: string }; response: void };
|
|
126
117
|
* }
|
|
127
118
|
*
|
|
128
|
-
* // Production usage
|
|
129
119
|
* const fetchNui = createFetchNui<MyEvents>();
|
|
130
120
|
* const player = await fetchNui("getPlayer", { id: 1 });
|
|
131
|
-
* // player.name is typed as string
|
|
132
121
|
*
|
|
133
|
-
* //
|
|
122
|
+
* // With mocks + debug
|
|
134
123
|
* const fetchNui = createFetchNui<MyEvents>({
|
|
135
124
|
* debug: true,
|
|
136
125
|
* mockData: {
|
|
@@ -148,26 +137,30 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
|
|
|
148
137
|
* end)
|
|
149
138
|
* ```
|
|
150
139
|
*/
|
|
151
|
-
declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, data?: TMap[K]["request"], options?: FetchNuiOptions) => Promise<TMap[K]["response"]>;
|
|
140
|
+
declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, ...args: TMap[K]["request"] extends void ? [data?: TMap[K]["request"], options?: FetchNuiOptions] : [data: TMap[K]["request"], options?: FetchNuiOptions]) => Promise<TMap[K]["response"]>;
|
|
152
141
|
|
|
153
142
|
/**
|
|
154
|
-
* Listens for NUI messages from Lua (`SendNUIMessage`)
|
|
155
|
-
* Only messages matching the given `action` trigger the handler.
|
|
156
|
-
* Returns a cleanup function to stop listening.
|
|
143
|
+
* Listens for NUI messages from Lua (`SendNUIMessage`).
|
|
157
144
|
*
|
|
158
|
-
*
|
|
145
|
+
* **Per-action** — filters by action name, `data` is fully typed:
|
|
159
146
|
* ```ts
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* hideMenu: { request: void; response: void };
|
|
163
|
-
* }
|
|
164
|
-
*
|
|
165
|
-
* const unsub = onNuiMessage<MyMessages, "showMenu">("showMenu", (data) => {
|
|
166
|
-
* console.log(data.items); // typed as string[]
|
|
147
|
+
* onNuiMessage<Events, "showMenu">("showMenu", (data) => {
|
|
148
|
+
* console.log(data.items); // ✅ typed as string[]
|
|
167
149
|
* });
|
|
150
|
+
* ```
|
|
168
151
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
152
|
+
* **Switch-case** — single listener for all actions:
|
|
153
|
+
* ```ts
|
|
154
|
+
* onNuiMessage<Events>((action, data) => {
|
|
155
|
+
* switch (action) {
|
|
156
|
+
* case "setLocales":
|
|
157
|
+
* registerLocales(data);
|
|
158
|
+
* break;
|
|
159
|
+
* case "showMenu":
|
|
160
|
+
* openMenu(data.items);
|
|
161
|
+
* break;
|
|
162
|
+
* }
|
|
163
|
+
* });
|
|
171
164
|
* ```
|
|
172
165
|
*
|
|
173
166
|
* Lua side:
|
|
@@ -176,13 +169,14 @@ declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: Fetch
|
|
|
176
169
|
* ```
|
|
177
170
|
*/
|
|
178
171
|
declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(action: K, handler: NuiMessageHandler<TMap[K]["request"]>): UnsubscribeFn;
|
|
172
|
+
declare function onNuiMessage<TMap extends NuiEventMap>(handler: (action: keyof TMap & string, data: any) => void): UnsubscribeFn;
|
|
179
173
|
|
|
180
174
|
/**
|
|
181
175
|
* Formats a string using Lua-style placeholders.
|
|
182
176
|
*
|
|
183
177
|
* Specifiers:
|
|
184
178
|
* - `%s` — string (null/undefined becomes empty string)
|
|
185
|
-
* - `%d` — integer (floors the value, NaN becomes 0)
|
|
179
|
+
* - `%d` / `%i` — integer (floors the value, NaN becomes 0)
|
|
186
180
|
* - `%f` — float (NaN becomes 0)
|
|
187
181
|
* - `%%` — literal percent sign
|
|
188
182
|
*
|
|
@@ -200,34 +194,75 @@ declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & s
|
|
|
200
194
|
*/
|
|
201
195
|
declare function luaFormat(template: string, ...args: FormatArg[]): string;
|
|
202
196
|
/**
|
|
203
|
-
* Creates
|
|
204
|
-
*
|
|
205
|
-
* The key supports dot-notation to traverse nested locale objects.
|
|
206
|
-
* If the key isn't found, the fallback string is returned as-is.
|
|
207
|
-
* Any extra args are passed through `luaFormat` for placeholder substitution.
|
|
197
|
+
* Creates an isolated translator bound to a specific locale record.
|
|
198
|
+
* Use this when you need a separate translator instance, independent of the global `_U`.
|
|
208
199
|
*
|
|
209
200
|
* @example
|
|
210
201
|
* ```ts
|
|
211
|
-
* const
|
|
202
|
+
* const _T = createTranslator({
|
|
212
203
|
* locales: {
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* level: "Level %d",
|
|
216
|
-
* },
|
|
217
|
-
* server: {
|
|
218
|
-
* error: "Error: %s",
|
|
219
|
-
* },
|
|
220
|
-
* flat_key: "Plain message",
|
|
204
|
+
* greeting: "Hello %s!",
|
|
205
|
+
* level: "Level %d",
|
|
221
206
|
* },
|
|
222
207
|
* });
|
|
223
208
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* _U("no.key", "Not found"); // → "Not found"
|
|
209
|
+
* _T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
|
|
210
|
+
* _T("level", "MISSING", 42); // → "Level 42"
|
|
211
|
+
* _T("no.key", "Not found"); // → "Not found"
|
|
228
212
|
* ```
|
|
229
213
|
*/
|
|
230
214
|
declare function createTranslator(options: TranslatorOptions): TranslatorFn;
|
|
215
|
+
/**
|
|
216
|
+
* Sets the global locale map. Call this when Lua sends locale data to the NUI.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* // Lua side:
|
|
221
|
+
* // SendNUIMessage({ action = "setLocales", data = locales })
|
|
222
|
+
*
|
|
223
|
+
* onNuiMessage<Events>((action, data) => {
|
|
224
|
+
* switch (action) {
|
|
225
|
+
* case "setLocales":
|
|
226
|
+
* registerLocales(data);
|
|
227
|
+
* break;
|
|
228
|
+
* }
|
|
229
|
+
* });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare function registerLocales(locales: LocaleRecord): void;
|
|
233
|
+
/**
|
|
234
|
+
* Merges new entries into the current global locale map without replacing it.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* registerLocales({ ui: { title: "Dashboard" } });
|
|
239
|
+
* extendLocales({ ui: { subtitle: "Overview" } });
|
|
240
|
+
*
|
|
241
|
+
* _U("ui.title", ""); // → "Dashboard"
|
|
242
|
+
* _U("ui.subtitle", ""); // → "Overview"
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
declare function extendLocales(...records: LocaleRecord[]): void;
|
|
246
|
+
/**
|
|
247
|
+
* Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.
|
|
248
|
+
*
|
|
249
|
+
* @param key Dot-notated key, e.g. `"ui.greeting"` or flat `"title"`
|
|
250
|
+
* @param fallback Returned as-is when the key doesn't exist
|
|
251
|
+
* @param args Values for `%s`, `%d`, `%f` placeholders
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```ts
|
|
255
|
+
* import { registerLocales, _U } from "@laot/nuix";
|
|
256
|
+
*
|
|
257
|
+
* // After Lua sends locales:
|
|
258
|
+
* // { ui: { greeting: "Hello %s!", level: "Level %d" } }
|
|
259
|
+
*
|
|
260
|
+
* _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
|
|
261
|
+
* _U("ui.level", "Lv.", 42); // → "Level 42"
|
|
262
|
+
* _U("missing.key", "Fallback"); // → "Fallback"
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
declare function _U(key: string, fallback: string, ...args: FormatArg[]): string;
|
|
231
266
|
/**
|
|
232
267
|
* Deep-merges multiple locale records into one.
|
|
233
268
|
* Later records override earlier ones on key conflicts.
|
|
@@ -245,4 +280,4 @@ declare function createTranslator(options: TranslatorOptions): TranslatorFn;
|
|
|
245
280
|
*/
|
|
246
281
|
declare function mergeLocales(...records: LocaleRecord[]): LocaleRecord;
|
|
247
282
|
|
|
248
|
-
export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, createFetchNui, createTranslator, luaFormat, mergeLocales, onNuiMessage };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@ function getResourceName() {
|
|
|
8
8
|
function createFetchNui(factoryOptions) {
|
|
9
9
|
const debug = factoryOptions?.debug ?? false;
|
|
10
10
|
const mockData = factoryOptions?.mockData;
|
|
11
|
-
return async function fetchNui(event,
|
|
11
|
+
return async function fetchNui(event, ...args) {
|
|
12
|
+
const [data, options] = args;
|
|
12
13
|
if (debug) {
|
|
13
14
|
console.log(`[NUIX] \u2192 ${event}`, data ?? {});
|
|
14
15
|
}
|
|
@@ -56,12 +57,17 @@ function createFetchNui(factoryOptions) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// src/listener.ts
|
|
59
|
-
function onNuiMessage(
|
|
60
|
+
function onNuiMessage(actionOrHandler, handler) {
|
|
60
61
|
const listener = (event) => {
|
|
61
62
|
const payload = event.data;
|
|
62
63
|
if (!payload || typeof payload !== "object") return;
|
|
63
|
-
if (payload.action
|
|
64
|
-
|
|
64
|
+
if (!payload.action) return;
|
|
65
|
+
if (typeof actionOrHandler === "string") {
|
|
66
|
+
if (payload.action !== actionOrHandler) return;
|
|
67
|
+
handler(payload.data);
|
|
68
|
+
} else {
|
|
69
|
+
actionOrHandler(payload.action, payload.data);
|
|
70
|
+
}
|
|
65
71
|
};
|
|
66
72
|
window.addEventListener("message", listener);
|
|
67
73
|
return () => {
|
|
@@ -113,12 +119,24 @@ function createTranslator(options) {
|
|
|
113
119
|
return args.length > 0 ? luaFormat(template, ...args) : template;
|
|
114
120
|
};
|
|
115
121
|
}
|
|
122
|
+
var _locales = {};
|
|
123
|
+
function registerLocales(locales) {
|
|
124
|
+
_locales = locales;
|
|
125
|
+
}
|
|
126
|
+
function extendLocales(...records) {
|
|
127
|
+
_locales = mergeLocales(_locales, ...records);
|
|
128
|
+
}
|
|
129
|
+
function _U(key, fallback, ...args) {
|
|
130
|
+
const template = resolveKey(_locales, key);
|
|
131
|
+
if (template === void 0) return fallback;
|
|
132
|
+
return args.length > 0 ? luaFormat(template, ...args) : template;
|
|
133
|
+
}
|
|
116
134
|
function mergeLocales(...records) {
|
|
117
135
|
const result = {};
|
|
118
136
|
for (const record of records) {
|
|
119
137
|
for (const [key, value] of Object.entries(record)) {
|
|
120
138
|
const existing = result[key];
|
|
121
|
-
if (typeof value === "object" && typeof existing === "object" && existing !== void 0) {
|
|
139
|
+
if (value !== null && existing !== null && typeof value === "object" && typeof existing === "object" && existing !== void 0) {
|
|
122
140
|
result[key] = mergeLocales(existing, value);
|
|
123
141
|
} else {
|
|
124
142
|
result[key] = value;
|
|
@@ -128,10 +146,13 @@ function mergeLocales(...records) {
|
|
|
128
146
|
return result;
|
|
129
147
|
}
|
|
130
148
|
export {
|
|
149
|
+
_U,
|
|
131
150
|
createFetchNui,
|
|
132
151
|
createTranslator,
|
|
152
|
+
extendLocales,
|
|
133
153
|
luaFormat,
|
|
134
154
|
mergeLocales,
|
|
135
|
-
onNuiMessage
|
|
155
|
+
onNuiMessage,
|
|
156
|
+
registerLocales
|
|
136
157
|
};
|
|
137
158
|
//# sourceMappingURL=index.js.map
|
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 fully typed `fetchNui` function tied to your event map.\r\n *\r\n * The returned function POSTs JSON to `https://<resourceName>/<event>`,\r\n * which maps 1:1 to `RegisterNUICallback` on the Lua side.\r\n *\r\n * Supports optional debug logging, mock data for local dev, and per-call timeout.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { msg: string }; response: void };\r\n * }\r\n *\r\n * // Production usage\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n * // player.name is typed as string\r\n *\r\n * // Local dev with mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (req) => { console.log(\"Mock:\", req.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\tdata?: TMap[K][\"request\"],\r\n\t\toptions?: FetchNuiOptions,\r\n\t): Promise<TMap[K][\"response\"]> {\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 (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\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`), filtered by action name.\r\n * Only messages matching the given `action` trigger the handler.\r\n * Returns a cleanup function to stop listening.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyMessages extends NuiEventMap {\r\n * showMenu: { request: { items: string[] }; response: void };\r\n * hideMenu: { request: void; response: void };\r\n * }\r\n *\r\n * const unsub = onNuiMessage<MyMessages, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // typed as string[]\r\n * });\r\n *\r\n * // Stop listening when done\r\n * unsub();\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\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"request\"]>,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload<TMap[K][\"request\"]>>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (payload.action !== action) return;\r\n\r\n\t\thandler(payload.data);\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` — 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 a `_U(key, fallback, ...args)` translator bound to a locale record.\r\n *\r\n * The key supports dot-notation to traverse nested locale objects.\r\n * If the key isn't found, the fallback string is returned as-is.\r\n * Any extra args are passed through `luaFormat` for placeholder substitution.\r\n *\r\n * @example\r\n * ```ts\r\n * const _U = createTranslator({\r\n * locales: {\r\n * client: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * server: {\r\n * error: \"Error: %s\",\r\n * },\r\n * flat_key: \"Plain message\",\r\n * },\r\n * });\r\n *\r\n * _U(\"client.greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"client.level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _U(\"flat_key\", \"MISSING\"); // → \"Plain message\"\r\n * _U(\"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// ─── 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 (typeof value === \"object\" && typeof existing === \"object\" && existing !== undefined) {\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// ─── 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: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { 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: (req) => { console.log(\"Mock:\", req.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][\"request\"] extends void\r\n\t\t\t? [data?: TMap[K][\"request\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"request\"], 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 (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\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][\"request\"]>,\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/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laot/nuix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Modular, type-safe TypeScript library for FiveM NUI projects",
|
|
5
|
+
"sideEffects": false,
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "./dist/index.cjs",
|
|
7
8
|
"module": "./dist/index.js",
|
|
@@ -42,6 +43,9 @@
|
|
|
42
43
|
"url": "https://github.com/laot7490/nuix/issues"
|
|
43
44
|
},
|
|
44
45
|
"license": "MIT",
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
45
49
|
"devDependencies": {
|
|
46
50
|
"tsup": "^8.4.0",
|
|
47
51
|
"typescript": "^5.7.3"
|