@laot/nuix 1.0.4 → 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 CHANGED
@@ -20,6 +20,8 @@ A type-safe TypeScript helper library for FiveM NUI development. Wraps the most
20
20
  - [Translator (Isolated)](#6-translator-isolated)
21
21
  - [Debug Mode](#7-debug-mode)
22
22
  - [Mock Data](#8-mock-data-local-development)
23
+ - [isEnvBrowser](#9-isenvbrowser--environment-detection)
24
+ - [disableMockInGame](#10-disablemockingame--mock-control-in-fivem)
23
25
  - [Lua Side](#lua-side)
24
26
  - [API Reference](#api-reference)
25
27
  - [Build](#build)
@@ -282,6 +284,48 @@ const player = await fetchNui("getPlayer", { id: 1 });
282
284
 
283
285
  ---
284
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
+
285
329
  ## Lua Side
286
330
 
287
331
  Here's how the Lua side connects to everything above:
@@ -308,7 +352,8 @@ SendNUIMessage({ action = "setLocales", data = Locales })
308
352
 
309
353
  | Export | Description |
310
354
  |---|---|
311
- | `createFetchNui<TMap>(options?)` | Returns a typed `fetchNui` function. Supports `debug` and `mockData` options. |
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). |
312
357
  | `onNuiMessage<TMap>(handler)` | Listens to all NUI messages — use with a switch-case. |
313
358
  | `onNuiMessage<TMap, K>(action, handler)` | Listens to a single action — `data` is automatically typed. |
314
359
  | `luaFormat(template, ...args)` | Lua-style string formatter with `%s` / `%d` / `%f` support. |
@@ -325,7 +370,7 @@ SendNUIMessage({ action = "setLocales", data = Locales })
325
370
  | `NuiEventMap` | Base interface for defining event maps. |
326
371
  | `NuiMessagePayload<TData>` | Shape of `SendNUIMessage` payloads (`{ action, data }`). |
327
372
  | `FetchNuiOptions` | Per-call options for `fetchNui` (e.g. `timeout`). |
328
- | `FetchNuiFactoryOptions<TMap>` | Config for `createFetchNui` (`debug`, `mockData`). |
373
+ | `FetchNuiFactoryOptions<TMap>` | Config for `createFetchNui` (`debug`, `mockData`, `disableMockInGame`). |
329
374
  | `LocaleRecord` | Flat or nested string map used for translations. |
330
375
  | `TranslatorOptions` | Config for `createTranslator`. |
331
376
  | `TranslatorFn` | Translator function signature (`(key, fallback, ...args) => string`). |
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
- if (mockData && event in mockData) {
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,
@@ -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
@@ -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
@@ -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
- if (mockData && event in mockData) {
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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laot/nuix",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Modular, type-safe TypeScript library for FiveM NUI projects",
5
5
  "sideEffects": false,
6
6
  "type": "module",