lt-script 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,269 +6,561 @@
6
6
  <img src="https://img.shields.io/badge/FiveM-Ready-green" alt="FiveM Ready">
7
7
  </p>
8
8
 
9
- **LT** is a modern, developer-friendly language that compiles to FiveM Lua. It brings TypeScript-like syntax, modern JavaScript features, and FiveM-specific sugar to the Lua ecosystem — making your scripts cleaner, safer, and more enjoyable to write.
9
+ **LT** is a modern, statically-typed superset of Lua designed specifically for **FiveM development**. It combines the elegance of TypeScript with the performance of Lua, adding FiveM-specific syntactic sugar to make your scripts robust, readable, and faster to write.
10
+
11
+ > **Goodbye boilerplate, hello productivity.**
12
+ > Write clean, type-safe code that compiles to optimized 100% vanilla Lua.
13
+
14
+ ---
10
15
 
11
16
  ## ✨ Why LT?
12
17
 
13
- Writing vanilla Lua for FiveM can be tedious. LT fixes that with:
18
+ * **🛡️ Type Safety**: Catch type mismatches and `nil` errors *before* you run the game.
19
+ * **⚡ Modern Syntax**: Use Arrow Functions, Destructuring, Spread syntax, and more.
20
+ * **🎮 FiveM-First**: Built-in keywords like `netevent`, `addcmd`, `thread`, and `wait`.
21
+ * **🔌 Zero Overhead**: Compiles to pure, readable Lua with no runtime dependencies.
14
22
 
15
- - **Modern Syntax**: Arrow functions, destructuring, spread operator, optional chaining
16
- - **FiveM Sugar**: Built-in `thread`, `wait`, `netevent`, `addcmd`, `export` keywords
17
- - **Type Safety**: Static type checking catches errors at compile time
18
- - **Clean Code**: No more boilerplate — just write what you mean
23
+ ---
19
24
 
20
25
  ## 📦 Installation
21
26
 
27
+ Prerequisites: [Node.js](https://nodejs.org/) (Version 16+)
28
+
22
29
  ```bash
30
+ # Global installation (Recommended)
23
31
  npm install -g lt-script
32
+
33
+ # OR run directly via npx
34
+ npx lt-script build .
24
35
  ```
25
36
 
37
+ ---
38
+
26
39
  ## 🚀 Quick Start
27
40
 
28
- Create a file called `main.lt`:
41
+ Create a `client.lt` file:
29
42
 
30
43
  ```typescript
31
- // Variables with modern scoping
32
- let health: number = 100
33
- const MAX_HEALTH = 200
44
+ // Define a type for player data
45
+ interface PlayerData {
46
+ id: number,
47
+ name: string,
48
+ health: number
49
+ }
34
50
 
35
- // Arrow functions
36
- let double = (x) => x * 2
51
+ // Variables with types
52
+ let active: boolean = false
53
+ const MAX_HEALTH = 100
37
54
 
38
- // String interpolation
39
- print("Health: $health / $MAX_HEALTH")
55
+ // FiveM Command
56
+ addcmd "heal" (source, args)
57
+ let amount = tonumber(args[1]) ?? MAX_HEALTH
58
+ SetEntityHealth(PlayerPedId(), amount)
59
+ print("Healed for ${amount}")
60
+ end
40
61
 
41
- // FiveM thread with wait
62
+ // Thread with loop
42
63
  thread
64
+ print("System active")
43
65
  loop (true)
44
66
  wait 1000
45
- print("Tick...")
67
+ if LocalPlayer.state?.isDead then
68
+ print("Player is dead!")
69
+ end
46
70
  end
47
71
  end
48
-
49
- // Register a command easily
50
- addcmd "heal" (source, args)
51
- health = MAX_HEALTH
52
- print("Player healed!")
53
- end
54
-
55
- // Network events without boilerplate
56
- netevent "player:spawn" (data)
57
- print("Player spawned: $data")
58
- end
59
72
  ```
60
73
 
61
- Compile it:
62
-
74
+ **Compile it:**
63
75
  ```bash
64
76
  ltc build .
65
77
  ```
66
78
 
67
- Output `main.lua`:
68
- ```lua
69
- local health = 100
70
- local MAX_HEALTH <const> = 200
79
+ ---
71
80
 
72
- local double = function(x) return x * 2 end
81
+ ## 📘 Language Reference
73
82
 
74
- print("Health: " .. tostring(health) .. " / " .. tostring(MAX_HEALTH))
83
+ ### 1. Variables & Types
75
84
 
76
- CreateThread(function()
77
- while true do
78
- Wait(1000)
79
- print("Tick...")
80
- end
81
- end)
85
+ LT supports explicit typing with compile-time validation.
82
86
 
83
- RegisterCommand("heal", function(source, args)
84
- health = MAX_HEALTH
85
- print("Player healed!")
86
- end, false)
87
-
88
- RegisterNetEvent("player:spawn")
89
- AddEventHandler("player:spawn", function(data)
90
- print("Player spawned: " .. tostring(data))
91
- end)
87
+ ```typescript
88
+ // Mutable variables
89
+ let score: number = 0
90
+ var playerName: string = "John" // Type inferred if omitted
91
+
92
+ // Constants (Compiles to Lua 5.4 <const>)
93
+ const MAX_PLAYERS = 32
94
+
95
+ // Available primitive types
96
+ let str: string = "hello"
97
+ let num: number = 42
98
+ let flag: boolean = true
99
+ let data: table = { key: "value" }
100
+ let pos: vector3 = <100.0, 200.0, 30.0>
101
+ let callback: function = () => print("hi")
102
+
103
+ // Arrays
104
+ let inventory: string[] = ["Apple", "Water"]
105
+ let numbers: number[] = [1, 2, 3]
92
106
  ```
93
107
 
94
- ## 🛠️ CLI Commands
108
+ ### 2. Interfaces & Type Aliases
95
109
 
96
- | Command | Description |
97
- |---------|-------------|
98
- | `ltc build <dir>` | Compile all `.lt` files in directory |
99
- | `ltc watch <dir>` | Watch mode — auto-compile on save |
110
+ Define complex data structures for compile-time validation.
100
111
 
101
- ## 🎯 Feature Highlights
102
-
103
- ### Variables & Constants
104
112
  ```typescript
105
- var globalVar = "I am global" // Global variable
106
- let localVar = 10 // Local variable
107
- const MAX = 100 // Constant (Lua 5.4 <const>)
108
- ```
113
+ // Interface - Object shape definition
114
+ interface VehicleConfig {
115
+ model: string,
116
+ price: number,
117
+ color: string
118
+ }
109
119
 
110
- ### Type System
111
- ```typescript
112
- // Explicit types
113
- let name: string = "John"
114
- let age: number = 25
115
- let active: boolean = true
120
+ // Type Alias - Alternative syntax
121
+ type PlayerPos = { x: number, y: number, z: number }
116
122
 
117
- // Type Aliases
118
- interface PlayerData = {
119
- name: string,
120
- level: number,
121
- isVip: boolean
123
+ // Usage with validation
124
+ const myCar: VehicleConfig = {
125
+ model: "sultan",
126
+ price: 50000,
127
+ color: "red"
122
128
  }
123
129
 
124
- func Calculate(a: number, b: number)
125
- return a + b
126
- end
127
- ```
130
+ // Arrays of interfaces
131
+ interface StoreConfig {
132
+ id: number,
133
+ position: vector3,
134
+ isOpen: boolean
135
+ }
128
136
 
129
- ### Arrow Functions
130
- ```typescript
131
- let add = (a, b) => a + b
132
- let greet = (name) => print("Hello, $name!")
137
+ const Stores: StoreConfig[] = [
138
+ { id: 1, position: <100.0, 200.0, 30.0>, isOpen: true },
139
+ { id: 2, position: <150.0, 250.0, 35.0>, isOpen: false }
140
+ ]
133
141
  ```
134
142
 
135
- ### Destructuring
143
+ > **Note:** Type validation ensures field types match. Assigning `number` to a `vector3` field will cause a compile error.
144
+
145
+ ### 3. Functions
146
+
136
147
  ```typescript
137
- let player = { name: "Alex", level: 50 }
138
- let { name, level } = player
148
+ // Standard function with typed parameters
149
+ func Add(a: number, b: number)
150
+ return a + b
151
+ end
152
+
153
+ // Default parameters
154
+ func Greet(name: string, prefix = "Hello")
155
+ print("${prefix}, ${name}!")
156
+ end
157
+
158
+ // Local function (Lua local function)
159
+ local function PrivateHelper()
160
+ return "internal"
161
+ end
139
162
 
140
- let coords = [100, 200, 300]
141
- let [x, y, z] = coords
163
+ // Arrow functions
164
+ let double = (x: number) => x * 2
165
+ let greet = (name) => print("Hello ${name}")
166
+
167
+ // Multi-line arrow
168
+ let process = (data) => {
169
+ let result = data * 2
170
+ return result
171
+ }
142
172
  ```
143
173
 
144
- ### Optional Chaining & Null Coalescing
174
+ ### 4. Control Flow
175
+
176
+ #### If / Else
145
177
  ```typescript
146
- let job = player?.metadata?.job?.label ?? "Unemployed"
178
+ // Note: 'then' keyword is required after condition
179
+ if health < 20 then
180
+ print("Low health!")
181
+ elseif health < 50 then
182
+ print("Medium health")
183
+ else
184
+ print("Full health")
185
+ end
147
186
  ```
148
187
 
149
- ### Vector Literals
188
+ #### Switch / Case
150
189
  ```typescript
151
- let pos = <100.0, 50.0, 20.0> // vector3
152
- let rot = <0.0, 90.0> // vector2
190
+ let job = "police"
191
+
192
+ switch job
193
+ case "police", "ambulance"
194
+ print("Government worker")
195
+ case "mechanic"
196
+ print("Service worker")
197
+ default
198
+ print("Civilian")
199
+ end
153
200
  ```
154
201
 
155
- ### Switch/Case
202
+ #### Guard Clauses
203
+ Clean up nested if-statements with early returns.
204
+
156
205
  ```typescript
157
- switch weather
158
- case "RAIN", "THUNDER"
159
- print("Stay inside!")
160
- case "CLEAR"
161
- print("Nice day!")
162
- default
163
- print("Unknown weather")
206
+ func RevivePlayer(player)
207
+ // Returns immediately if player is nil
208
+ guard player return
209
+
210
+ // Returns with custom logic if condition fails
211
+ guard player.isDead else
212
+ print("Player is already alive")
213
+ return
214
+ end
215
+
216
+ // Main logic
217
+ player.revive()
218
+ end
219
+
220
+ // Guard with return value
221
+ func GetPlayer(id)
222
+ let player = GetPlayerById(id)
223
+ guard player return nil
224
+ return player
164
225
  end
165
226
  ```
166
227
 
167
- ### For Loops
228
+ #### For Loops
168
229
  ```typescript
169
- // Range loop
170
- for i in 1..10 do
230
+ // Numeric for (start, end)
231
+ for i = 1, 10 do
171
232
  print(i)
172
233
  end
173
234
 
174
- // Range with step
175
- for i in 10..1 by -1 do
235
+ // Numeric for with step (start, end, step)
236
+ for i = 0, 100, 5 do
176
237
  print(i)
177
238
  end
178
239
 
179
- // C-style loop
180
- for i = 0, 100, 5 do
240
+ // Range for with step
241
+ for i in 1..10 by 2 do
181
242
  print(i)
182
243
  end
244
+
245
+ // For-in (pairs)
246
+ for key, value in pairs(myTable) do
247
+ print("${key}: ${value}")
248
+ end
249
+
250
+ // For-in (ipairs)
251
+ for index, item in ipairs(myArray) do
252
+ print("${index}: ${item}")
253
+ end
254
+ ```
255
+
256
+ #### While Loop
257
+ ```typescript
258
+ let count = 0
259
+ while count < 10 do
260
+ print(count)
261
+ count += 1
262
+ end
263
+ ```
264
+
265
+ ### 5. Modern Operators
266
+
267
+ #### Compound Assignment
268
+ ```typescript
269
+ let x = 10
270
+ x += 5
271
+ x -= 3
272
+ x *= 2
273
+ x /= 4
274
+ x %= 3
275
+ ```
276
+
277
+ #### Null Coalescing (`??`)
278
+ ```typescript
279
+ let name = playerName ?? "Unknown"
280
+ let health = GetHealth() ?? 100
183
281
  ```
184
282
 
185
- ### FiveM Specific
283
+ #### Optional Chaining (`?.`)
284
+ ```typescript
285
+ let state = LocalPlayer.state?.isDead
286
+ let nested = obj?.deep?.value
287
+ let arrItem = arr?[0]
288
+ ```
289
+
290
+ #### Ternary Operator
291
+ ```typescript
292
+ let status = isDead ? "Dead" : "Alive"
293
+ let label = count > 0 ? "${count} items" : "Empty"
294
+ ```
295
+
296
+ ### 6. Destructuring & Spread
297
+
298
+ ```typescript
299
+ // Object Destructuring
300
+ let { x, y, z } = GetEntityCoords(ped)
301
+ let { name, health } = playerData
302
+
303
+ // Array Destructuring
304
+ let [first, second] = GetValues()
305
+
306
+ // Object Spread (tables only)
307
+ let base = { a: 1, b: 2 }
308
+ let extended = { ...base, c: 3 }
309
+ ```
310
+
311
+ ### 7. String Interpolation
312
+
313
+ Use `${expression}` inside strings to embed values:
314
+
315
+ ```typescript
316
+ let name = "John"
317
+ let age = 25
318
+
319
+ print("Hello, ${name}!")
320
+ print("You are ${age} years old")
321
+ print("Next year you'll be ${age + 1}")
322
+
323
+ // Complex expressions
324
+ let player = { health: 100 }
325
+ print("Health: ${player.health}")
326
+ ```
327
+
328
+ ### 8. Vector Literals
329
+
330
+ ```typescript
331
+ // Vector3 (FiveM native)
332
+ let pos: vector3 = <100.0, 200.0, 30.0>
333
+
334
+ // Vector2
335
+ let screenPos: vector2 = <0.5, 0.5>
336
+
337
+ // Vector4 (includes heading)
338
+ let spawn: vector4 = <100.0, 200.0, 30.0, 90.0>
339
+ ```
340
+
341
+ ---
342
+
343
+ ## 🎮 FiveM Features
344
+
345
+ ### Thread & Loop
186
346
 
187
- #### Threads & Wait
188
347
  ```typescript
348
+ // Create a thread (Citizen.CreateThread)
189
349
  thread
190
- wait 1000
191
- print("After 1 second")
350
+ print("Thread started")
351
+
352
+ loop (true)
353
+ wait 0 // Run every frame
354
+
355
+ // Game logic here
356
+ let ped = PlayerPedId()
357
+ let health = GetEntityHealth(ped)
358
+
359
+ if health < 50 then
360
+ print("Low health warning!")
361
+ end
362
+ end
192
363
  end
193
364
  ```
194
365
 
195
- #### Events
366
+ ### Wait
367
+
196
368
  ```typescript
197
- netevent "myResource:sync" (data)
198
- -- RegisterNetEvent + AddEventHandler
369
+ // Wait in milliseconds
370
+ wait 1000 // Wait 1 second
371
+ wait 0 // Yield one frame
372
+ ```
373
+
374
+ ### Network Events
375
+
376
+ ```typescript
377
+ // Register + Handle network event (Client <-> Server)
378
+ netevent "bank:deposit" (amount)
379
+ print("Depositing ${amount}")
380
+ UpdateBankBalance(amount)
199
381
  end
200
382
 
201
- event "localEvent" (data)
202
- -- AddEventHandler only
383
+ // Local event handler
384
+ event "onClientResourceStart" (resName)
385
+ guard resName == GetCurrentResourceName() return
386
+ print("Resource started!")
203
387
  end
204
388
  ```
205
389
 
206
- #### Commands
390
+ ### Emit Events
391
+
392
+ ```typescript
393
+ // Emit to server
394
+ emit "server:event:name" (arg1, arg2)
395
+
396
+ // Explicit emit variants
397
+ emitServer "myevent" (data)
398
+ emitClient "myevent" (targetPlayer, data)
399
+ ```
400
+
401
+ ### Commands
402
+
207
403
  ```typescript
404
+ // Register a command (RegisterCommand wrapper)
208
405
  addcmd "teleport" (source, args)
209
- print("Teleporting...")
406
+ let x = tonumber(args[1]) ?? 0
407
+ let y = tonumber(args[2]) ?? 0
408
+ let z = tonumber(args[3]) ?? 0
409
+
410
+ SetEntityCoords(PlayerPedId(), x, y, z)
411
+ print("Teleported to ${x}, ${y}, ${z}")
210
412
  end
211
413
  ```
212
414
 
213
- #### Exports
415
+ ### Exports
416
+
214
417
  ```typescript
215
- export func GetPlayerData(id)
216
- return players[id]
418
+ // Export a function
419
+ export "GetPlayerMoney" ()
420
+ return playerMoney
217
421
  end
218
422
  ```
219
423
 
220
- #### Guard Statements
424
+ ### Timeout & Interval
425
+
221
426
  ```typescript
222
- guard player return // Early return if nil/false
223
- guard player.job == "police" return print("Access denied")
427
+ // Run once after delay
428
+ timeout 5000
429
+ print("5 seconds passed!")
430
+ end
224
431
 
225
- // Guard with else block
226
- guard isValid else
227
- print("Invalid state")
228
- return
432
+ // Run repeatedly
433
+ interval 1000
434
+ print("Every second")
229
435
  end
230
436
  ```
231
437
 
232
- #### Try/Catch
438
+ ### Try / Catch
439
+
233
440
  ```typescript
234
441
  try
235
- riskyOperation()
442
+ RiskyOperation()
236
443
  catch err
237
- print("Error: $err")
444
+ print("Error: ${err}")
445
+ end
446
+ ```
447
+
448
+ ---
449
+
450
+ ## ⚡ Complete Example: Store Robbery System
451
+
452
+ ```typescript
453
+ // 1. Type Definition
454
+ interface StoreConfig {
455
+ id: number,
456
+ position: vector3,
457
+ reward: number,
458
+ isOpen: boolean
459
+ }
460
+
461
+ // 2. Configuration
462
+ const Stores: StoreConfig[] = [
463
+ { id: 1, position: <120.5, -500.2, 30.0>, reward: 5000, isOpen: true },
464
+ { id: 2, position: <250.0, 300.0, 25.0>, reward: 10000, isOpen: true }
465
+ ]
466
+
467
+ let isRobbing = false
468
+
469
+ // 3. Main Game Loop
470
+ thread
471
+ loop (true)
472
+ wait 0
473
+
474
+ if isRobbing then
475
+ break
476
+ end
477
+
478
+ let ped = PlayerPedId()
479
+ let pos = GetEntityCoords(ped)
480
+
481
+ for _, store in ipairs(Stores) do
482
+ if not store.isOpen then
483
+ break
484
+ end
485
+
486
+ let distance = #(pos - store.position)
487
+ if distance < 2.0 then
488
+ ShowHelpText("Press ~INPUT_CONTEXT~ to rob")
489
+
490
+ if IsControlJustPressed(0, 38) then
491
+ StartRobbery(store)
492
+ end
493
+ end
494
+ end
495
+ end
496
+ end
497
+
498
+ // 4. Robbery Function
499
+ func StartRobbery(store: StoreConfig)
500
+ isRobbing = true
501
+
502
+ // Notify server
503
+ emit "robbery:start" (store.id)
504
+
505
+ // Play animation
506
+ TaskPlayAnim(PlayerPedId(), "anim_dict", "anim_name", 8.0, -8.0, -1, 0, 0, false, false, false)
507
+
508
+ wait 5000
509
+
510
+ ClearPedTasks(PlayerPedId())
511
+ isRobbing = false
512
+
513
+ print("Robbery complete! Reward: ${store.reward}")
514
+ end
515
+
516
+ // 5. Server Sync Handler
517
+ netevent "robbery:sync" (storeId, newState)
518
+ let store = Stores.find(s => s.id == storeId)
519
+ guard store return
520
+
521
+ store.isOpen = newState
522
+ let stateStr = newState ? "open" : "closed"
523
+ print("Store ${storeId} is now ${stateStr}")
238
524
  end
239
525
  ```
240
526
 
241
- ## 📁 Project Structure
527
+ ---
528
+
529
+ ## 🛠️ CLI Reference
530
+
531
+ | Command | Usage | Description |
532
+ |:--------|:------|:------------|
533
+ | **Build** | `ltc build ./` | Compiles all `.lt` files in the folder to `.lua` |
534
+ | **Watch** | `ltc watch ./` | Watches for file changes and recompiles instantly |
535
+ | **Version** | `ltc -v` | Displays the current compiler version |
536
+
537
+ ### Project Structure
538
+
539
+ LT automatically detects `src/` folders:
242
540
 
243
541
  ```
244
542
  my-resource/
245
543
  ├── src/
246
544
  │ ├── client.lt
247
545
  │ └── server.lt
248
- ├── client.lua (generated)
249
- ├── server.lua (generated)
250
- └── fxmanifest.lua
546
+ ├── client.lua ← Generated
547
+ └── server.lua ← Generated
251
548
  ```
252
549
 
253
- Run `ltc watch .` in your resource folder for automatic compilation.
550
+ ---
254
551
 
255
552
  ## 🔧 VS Code Extension
256
553
 
257
- Get syntax highlighting and IntelliSense for `.lt` files:
258
-
259
- **[LT Language Extension](https://marketplace.visualstudio.com/items?itemName=laot.lt-language)**
260
-
261
- - Syntax Highlighting
262
- - IntelliSense for Variables & Functions
263
- - **Native FiveM Autocompletion** (GetHashKey, CreateVehicle, etc.)
264
- - Snippets for common patterns
265
-
266
- ## 📜 License
554
+ Get the official **[LT Language Extension](https://marketplace.visualstudio.com/items?itemName=laot.lt-language)** for the best experience.
267
555
 
268
- MIT © [LaotScripts](https://github.com/laot7490)
556
+ **Features:**
557
+ * 🎨 Full Syntax Highlighting
558
+ * 🔍 Intelligent Autocompletion
559
+ * 📦 **FiveM Native Intellisense**
560
+ * ✨ Real-time Error Checking
269
561
 
270
562
  ---
271
563
 
272
564
  <p align="center">
273
- Made with ❤️ for the FiveM community
565
+ Made with ❤️ for the FiveM Community by <b>LaotScripts</b>
274
566
  </p>
@@ -149,6 +149,7 @@ export class LuaEmitter {
149
149
  this.emitCommandStmt(stmt);
150
150
  break;
151
151
  case AST.NodeType.TypeDecl:
152
+ case AST.NodeType.TypeAliasDecl:
152
153
  // Type declarations are compile-time only, no Lua output
153
154
  break;
154
155
  default:
@@ -29,6 +29,7 @@ export declare enum NodeType {
29
29
  EventHandler = "EventHandler",
30
30
  ExportDecl = "ExportDecl",
31
31
  TypeDecl = "TypeDecl",
32
+ TypeAliasDecl = "TypeAliasDecl",
32
33
  BinaryExpr = "BinaryExpr",
33
34
  UnaryExpr = "UnaryExpr",
34
35
  UpdateExpr = "UpdateExpr",// ++, --
@@ -93,6 +94,11 @@ export interface TypeDecl extends Statement {
93
94
  name: Identifier;
94
95
  fields: TypeField[];
95
96
  }
97
+ export interface TypeAliasDecl extends Statement {
98
+ kind: NodeType.TypeAliasDecl;
99
+ name: Identifier;
100
+ type: string;
101
+ }
96
102
  export interface AssignmentStmt extends Statement {
97
103
  kind: NodeType.AssignmentStmt;
98
104
  targets: Expression[];
@@ -35,6 +35,7 @@ export var NodeType;
35
35
  NodeType["ExportDecl"] = "ExportDecl";
36
36
  // Type System
37
37
  NodeType["TypeDecl"] = "TypeDecl";
38
+ NodeType["TypeAliasDecl"] = "TypeAliasDecl";
38
39
  // Expressions
39
40
  NodeType["BinaryExpr"] = "BinaryExpr";
40
41
  NodeType["UnaryExpr"] = "UnaryExpr";
@@ -10,6 +10,8 @@ export declare class Parser {
10
10
  constructor(tokens: Token[]);
11
11
  parse(): AST.Program;
12
12
  private parseStatement;
13
+ private parseType;
14
+ private parseTypeAlias;
13
15
  private parseVariableDecl;
14
16
  private parseIfStmt;
15
17
  private parseForStmt;
@@ -19,6 +19,18 @@ export class Parser {
19
19
  }
20
20
  // ============ Statement Parsing ============
21
21
  parseStatement() {
22
+ // Ambiguity Check: 'type' Identifier = ... VS type(x)
23
+ if (this.at().type === TokenType.IDENTIFIER && this.at().value === 'type') {
24
+ // Lookahead to see if it's a declaration
25
+ const next = this.tokens[this.pos + 1];
26
+ if (next && next.type === TokenType.IDENTIFIER) {
27
+ const nextNext = this.tokens[this.pos + 2];
28
+ if (nextNext && nextNext.type === TokenType.EQUALS) {
29
+ this.eat(); // consume 'type' pseudo-keyword
30
+ return this.parseTypeAlias();
31
+ }
32
+ }
33
+ }
22
34
  switch (this.at().type) {
23
35
  case TokenType.VAR:
24
36
  case TokenType.LET:
@@ -77,6 +89,65 @@ export class Parser {
77
89
  return this.parseExpressionStatement();
78
90
  }
79
91
  }
92
+ // ============ Type Parsing ============
93
+ parseType() {
94
+ // If we're parsing a type and hit a '{', it's an object type literal: value: { a: number }
95
+ if (this.at().type === TokenType.LBRACE) {
96
+ this.eat(); // {
97
+ let typeBody = "{ ";
98
+ while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
99
+ const key = this.parseIdentifier().name;
100
+ let val = "any";
101
+ // Check for optional property
102
+ if (this.match(TokenType.QUESTION)) {
103
+ key + "?";
104
+ }
105
+ if (this.match(TokenType.COLON)) {
106
+ val = this.parseType();
107
+ }
108
+ typeBody += `${key}: ${val}`;
109
+ if (!this.check(TokenType.RBRACE)) {
110
+ this.match(TokenType.COMMA) || this.match(TokenType.SEMICOLON);
111
+ typeBody += ", ";
112
+ }
113
+ }
114
+ this.expect(TokenType.RBRACE);
115
+ typeBody += " }";
116
+ // Handle array of objects: { ... }[]
117
+ while (this.match(TokenType.LBRACKET)) {
118
+ this.expect(TokenType.RBRACKET);
119
+ typeBody += "[]";
120
+ }
121
+ return typeBody;
122
+ }
123
+ const typeToken = this.eat();
124
+ let typeName = typeToken.value;
125
+ // Handle generic types: List<string> (Basic support)
126
+ if (this.match(TokenType.LT_ANGLE)) { // <
127
+ typeName += "<";
128
+ typeName += this.parseType();
129
+ while (this.match(TokenType.COMMA)) {
130
+ typeName += ", ";
131
+ typeName += this.parseType();
132
+ }
133
+ this.expect(TokenType.GT_ANGLE); // >
134
+ typeName += ">";
135
+ }
136
+ // Handle array types: string[] or string[][]
137
+ while (this.match(TokenType.LBRACKET)) {
138
+ this.expect(TokenType.RBRACKET);
139
+ typeName += "[]";
140
+ }
141
+ return typeName;
142
+ }
143
+ parseTypeAlias() {
144
+ // 'type' is already consumed or handled by caller
145
+ const name = this.parseIdentifier();
146
+ this.expect(TokenType.EQUALS);
147
+ const typeStr = this.parseType();
148
+ return { kind: AST.NodeType.TypeAliasDecl, name, type: typeStr };
149
+ }
150
+ // ============ Statement Parsing ============
80
151
  parseVariableDecl() {
81
152
  const scopeToken = this.eat();
82
153
  const scope = scopeToken.value;
@@ -105,12 +176,12 @@ export class Parser {
105
176
  };
106
177
  names.push(parseName());
107
178
  if (this.match(TokenType.COLON)) {
108
- typeAnnotations[0] = this.eat().value;
179
+ typeAnnotations[0] = this.parseType();
109
180
  }
110
181
  while (this.match(TokenType.COMMA)) {
111
182
  names.push(parseName());
112
183
  if (this.match(TokenType.COLON)) {
113
- typeAnnotations[names.length - 1] = this.eat().value;
184
+ typeAnnotations[names.length - 1] = this.parseType();
114
185
  }
115
186
  }
116
187
  let values;
@@ -376,7 +447,7 @@ export class Parser {
376
447
  this.expect(TokenType.RPAREN);
377
448
  let returnType;
378
449
  if (this.match(TokenType.COLON)) {
379
- returnType = this.eat().value;
450
+ returnType = this.parseType();
380
451
  }
381
452
  const body = this.parseBlockUntil(TokenType.END);
382
453
  this.expect(TokenType.END);
@@ -399,7 +470,7 @@ export class Parser {
399
470
  this.expect(TokenType.RPAREN);
400
471
  let returnType;
401
472
  if (this.match(TokenType.COLON)) {
402
- returnType = this.eat().value;
473
+ returnType = this.parseType();
403
474
  }
404
475
  const body = this.parseBlockUntil(TokenType.END);
405
476
  this.expect(TokenType.END);
@@ -460,13 +531,12 @@ export class Parser {
460
531
  parseTypeDecl() {
461
532
  this.eat(); // interface
462
533
  const name = this.parseIdentifier();
463
- this.expect(TokenType.EQUALS);
464
534
  this.expect(TokenType.LBRACE);
465
535
  const fields = [];
466
536
  while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
467
537
  const fieldName = this.parseIdentifier().name;
468
538
  this.expect(TokenType.COLON);
469
- const fieldType = this.eat().value; // Get the type as string
539
+ const fieldType = this.parseType(); // Get the type as string
470
540
  fields.push({ name: fieldName, type: fieldType });
471
541
  // Allow trailing comma
472
542
  if (!this.check(TokenType.RBRACE)) {
@@ -913,7 +983,7 @@ export class Parser {
913
983
  const name = this.parseIdentifierName();
914
984
  let typeAnnotation;
915
985
  if (this.match(TokenType.COLON)) {
916
- typeAnnotation = this.eat().value;
986
+ typeAnnotation = this.parseType();
917
987
  }
918
988
  let defaultValue;
919
989
  if (this.match(TokenType.EQUALS)) {
@@ -3,7 +3,9 @@ export declare class SemanticAnalyzer {
3
3
  private globalScope;
4
4
  private currentScope;
5
5
  private typeRegistry;
6
+ private typeAliasRegistry;
6
7
  private memberTypes;
8
+ private functionRegistry;
7
9
  analyze(program: AST.Program): void;
8
10
  private enterScope;
9
11
  private exitScope;
@@ -18,6 +20,9 @@ export declare class SemanticAnalyzer {
18
20
  private visitExpression;
19
21
  private inferType;
20
22
  private stringToType;
23
+ private visitTypeAliasDecl;
21
24
  private visitTypeDecl;
25
+ private validateTypeExists;
22
26
  private validateTableAgainstType;
27
+ private parseObjectLiteralType;
23
28
  }
@@ -23,14 +23,31 @@ class Scope {
23
23
  export class SemanticAnalyzer {
24
24
  globalScope = new Scope();
25
25
  currentScope = this.globalScope;
26
- typeRegistry = new Map();
27
- memberTypes = new Map(); // Tracks Table.Field -> type mapping
26
+ typeRegistry = new Map(); // Stores interface fields
27
+ typeAliasRegistry = new Map(); // Stores type alias strings
28
+ memberTypes = new Map(); // Tracks Table.Field -> type mappings
29
+ functionRegistry = new Map(); // Stores function signatures
28
30
  analyze(program) {
29
31
  // Reset scope for fresh analysis (though typically instance is fresh)
30
32
  this.currentScope = this.globalScope;
31
- // Visit all statements
33
+ this.typeRegistry.clear();
34
+ this.typeAliasRegistry.clear();
35
+ this.memberTypes.clear();
36
+ this.functionRegistry.clear();
37
+ // First pass: Register types (to allow forward references)
32
38
  for (const stmt of program.body) {
33
- this.visitStatement(stmt);
39
+ if (stmt.kind === AST.NodeType.TypeDecl) {
40
+ this.visitTypeDecl(stmt);
41
+ }
42
+ else if (stmt.kind === AST.NodeType.TypeAliasDecl) {
43
+ this.visitTypeAliasDecl(stmt);
44
+ }
45
+ }
46
+ // Second pass: Analyze code
47
+ for (const stmt of program.body) {
48
+ if (stmt.kind !== AST.NodeType.TypeDecl && stmt.kind !== AST.NodeType.TypeAliasDecl) {
49
+ this.visitStatement(stmt);
50
+ }
34
51
  }
35
52
  }
36
53
  enterScope() {
@@ -117,6 +134,29 @@ export class SemanticAnalyzer {
117
134
  this.exitScope();
118
135
  }
119
136
  break;
137
+ case AST.NodeType.EventHandler:
138
+ {
139
+ const s = stmt;
140
+ this.enterScope();
141
+ s.params.forEach(p => this.currentScope.define(p.name.name, 'param', 'any', s.line || 0));
142
+ this.visitBlock(s.body);
143
+ this.exitScope();
144
+ }
145
+ break;
146
+ case AST.NodeType.TryCatchStmt:
147
+ {
148
+ const s = stmt;
149
+ this.visitBlock(s.tryBody);
150
+ this.enterScope();
151
+ this.currentScope.define(s.catchParam.name, 'let', 'any', s.line || 0);
152
+ this.visitBlock(s.catchBody);
153
+ this.exitScope();
154
+ }
155
+ break;
156
+ // Handle expression statements (standalone function calls like ProcessNumber("hello"))
157
+ case AST.NodeType.CallExpr:
158
+ this.visitExpression(stmt);
159
+ break;
120
160
  // Basic fallback recursive visiting
121
161
  default:
122
162
  // Identify other nodes that contain blocks/expressions to visit
@@ -171,8 +211,30 @@ export class SemanticAnalyzer {
171
211
  if (decl.values && decl.values[index]) {
172
212
  const value = decl.values[index];
173
213
  const inferred = this.inferType(value);
174
- // If we have a custom type annotation and the value is a table, validate it
175
- if (customTypeName && value.kind === AST.NodeType.TableLiteral) {
214
+ const annotation = decl.typeAnnotations && decl.typeAnnotations[index];
215
+ // If it's an array type (e.g., RobberyConfig[] or number[]) and value is a table, validate each element
216
+ if (annotation && annotation.endsWith('[]') && value.kind === AST.NodeType.TableLiteral) {
217
+ const elementTypeName = annotation.slice(0, -2); // Remove []
218
+ const expectedElementType = this.stringToType(elementTypeName);
219
+ const tableValue = value;
220
+ // Iterate through each element and validate
221
+ for (let i = 0; i < tableValue.fields.length; i++) {
222
+ const field = tableValue.fields[i];
223
+ // If element is a TableLiteral and element type is an interface, validate against interface
224
+ if (field.value.kind === AST.NodeType.TableLiteral && this.typeRegistry.has(elementTypeName)) {
225
+ this.validateTableAgainstType(field.value, elementTypeName, decl.line || 0);
226
+ }
227
+ else {
228
+ // For primitive types (number[], string[], etc.), check each element type
229
+ const actualType = this.inferType(field.value);
230
+ if (expectedElementType !== 'any' && actualType !== 'any' && expectedElementType !== actualType) {
231
+ throw new Error(`Type mismatch in array at line ${decl.line}: Element ${i + 1} expected '${elementTypeName}', got '${actualType}'`);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ // If we have a custom type annotation and the value is a table (not array), validate it
237
+ else if (customTypeName && value.kind === AST.NodeType.TableLiteral) {
176
238
  this.validateTableAgainstType(value, customTypeName, decl.line || 0);
177
239
  }
178
240
  // If we have an annotation, check compatibility
@@ -221,8 +283,36 @@ export class SemanticAnalyzer {
221
283
  }
222
284
  }
223
285
  }
224
- // Handle member expression type checking (Config.Field: type = value)
286
+ // Handle member expression type checking
225
287
  if (t.kind === AST.NodeType.MemberExpr) {
288
+ const member = t;
289
+ // 1. Strict Property Existence Check
290
+ if (member.object.kind === AST.NodeType.Identifier && !member.computed) {
291
+ const objName = member.object.name;
292
+ const sym = this.currentScope.lookup(objName);
293
+ const propName = member.property.name;
294
+ if (sym && sym.type !== 'any' && sym.type !== 'table') {
295
+ // Check if it's an interface or alias
296
+ let fields;
297
+ // Remove array suffix for type lookup if needed (though variable shouldn't be array if accessing prop directly)
298
+ const baseType = sym.type.replace('[]', '');
299
+ if (this.typeRegistry.has(baseType)) {
300
+ fields = this.typeRegistry.get(baseType);
301
+ }
302
+ else if (this.typeAliasRegistry.has(baseType)) {
303
+ const aliasStr = this.typeAliasRegistry.get(baseType);
304
+ if (aliasStr.startsWith('{')) {
305
+ fields = this.parseObjectLiteralType(aliasStr);
306
+ }
307
+ }
308
+ if (fields) {
309
+ const fieldExists = fields.some(f => f.name === propName);
310
+ if (!fieldExists) {
311
+ throw new Error(`Property '${propName}' does not exist on type '${sym.type}' at line ${stmt.line}`);
312
+ }
313
+ }
314
+ }
315
+ }
226
316
  const memberKey = this.getMemberExprKey(t);
227
317
  // If this assignment has a type annotation, register it
228
318
  if (stmt.typeAnnotation && index === 0) {
@@ -279,8 +369,22 @@ export class SemanticAnalyzer {
279
369
  }
280
370
  visitFunctionDecl(decl) {
281
371
  // Name is defined in CURRENT scope
372
+ let funcName;
282
373
  if (decl.name && decl.name.kind === AST.NodeType.Identifier) {
283
- this.currentScope.define(decl.name.name, 'const', 'function', decl.line || 0);
374
+ funcName = decl.name.name;
375
+ this.currentScope.define(funcName, 'const', 'function', decl.line || 0);
376
+ }
377
+ // Register function signature for call validation
378
+ const paramTypes = [];
379
+ decl.params.forEach(p => {
380
+ let type = 'any';
381
+ if (p.typeAnnotation) {
382
+ type = this.stringToType(p.typeAnnotation);
383
+ }
384
+ paramTypes.push({ name: p.name.name, type });
385
+ });
386
+ if (funcName) {
387
+ this.functionRegistry.set(funcName, { params: paramTypes });
284
388
  }
285
389
  this.enterScope();
286
390
  decl.params.forEach(p => {
@@ -310,6 +414,41 @@ export class SemanticAnalyzer {
310
414
  // Mostly just traversing to find nested scopes in functions/arrows
311
415
  if (!expr)
312
416
  return;
417
+ // Check for undefined variable usage
418
+ if (expr.kind === AST.NodeType.Identifier) {
419
+ const id = expr;
420
+ const sym = this.currentScope.lookup(id.name);
421
+ // Skip globals/builtins - only check if it looks like a user variable
422
+ // FiveM/Lua has many globals, so we only warn for simple lowercase identifiers
423
+ // that are not common Lua globals
424
+ const luaGlobals = ['print', 'type', 'tostring', 'tonumber', 'pairs', 'ipairs',
425
+ 'next', 'error', 'assert', 'pcall', 'xpcall', 'require',
426
+ 'table', 'string', 'math', 'os', 'io', 'coroutine', 'debug',
427
+ 'setmetatable', 'getmetatable', 'rawget', 'rawset', 'rawequal',
428
+ 'collectgarbage', 'dofile', 'load', 'loadfile', 'select',
429
+ 'unpack', '_G', '_VERSION', 'true', 'false', 'nil',
430
+ // FiveM common globals
431
+ 'Citizen', 'CreateThread', 'Wait', 'GetGameTimer', 'vector3',
432
+ 'vector2', 'vector4', 'GetCurrentResourceName', 'exports',
433
+ 'PlayerPedId', 'GetPlayerPed', 'GetEntityCoords', 'TriggerEvent',
434
+ 'TriggerServerEvent', 'TriggerClientEvent', 'RegisterNetEvent',
435
+ 'AddEventHandler', 'RegisterCommand', 'IsControlJustPressed',
436
+ 'AddEventHandler', 'RegisterCommand', 'LocalPlayer', 'PlayerId',
437
+ 'GetPlayerServerId', 'source', 'SetEntityHealth', 'ESX', 'QBCore',
438
+ // Additional FiveM/Lua globals
439
+ 'Config', 'Framework', 'MySQL', 'lib', 'cache', 'json',
440
+ 'NetworkGetNetworkIdFromEntity', 'NetworkGetEntityFromNetworkId',
441
+ 'GetPlayerPed', 'GetVehiclePedIsIn', 'IsPedInAnyVehicle',
442
+ 'SetPedCanRagdoll', 'SetTimeout', 'SetInterval', 'ClearTimeout',
443
+ 'GetHashKey', 'IsModelValid', 'RequestModel', 'HasModelLoaded'];
444
+ if (!sym && !luaGlobals.includes(id.name)) {
445
+ // Check if it's a type name (not a variable)
446
+ if (!this.typeRegistry.has(id.name) && !this.typeAliasRegistry.has(id.name)) {
447
+ throw new Error(`Undefined variable '${id.name}' at line ${id.line ?? 0}`);
448
+ }
449
+ }
450
+ return;
451
+ }
313
452
  if (expr.kind === AST.NodeType.ArrowFunc) {
314
453
  const arrow = expr;
315
454
  this.enterScope();
@@ -335,8 +474,57 @@ export class SemanticAnalyzer {
335
474
  this.visitFunctionDecl(expr);
336
475
  return;
337
476
  }
338
- // Recurse
339
- this.visitChildren(expr);
477
+ // Handle TableLiteral - recurse into field values only
478
+ if (expr.kind === AST.NodeType.TableLiteral) {
479
+ const table = expr;
480
+ for (const field of table.fields) {
481
+ // Don't check keys - they are field names, not variable references
482
+ // Only check computed keys like [expression] or string keys
483
+ if (field.key && field.key.kind !== AST.NodeType.Identifier) {
484
+ this.visitExpression(field.key);
485
+ }
486
+ this.visitExpression(field.value);
487
+ }
488
+ return;
489
+ }
490
+ // Handle BinaryExpr
491
+ if (expr.kind === AST.NodeType.BinaryExpr) {
492
+ const bin = expr;
493
+ this.visitExpression(bin.left);
494
+ this.visitExpression(bin.right);
495
+ return;
496
+ }
497
+ // Handle CallExpr - recurse into callee and args, validate argument types
498
+ if (expr.kind === AST.NodeType.CallExpr) {
499
+ const call = expr;
500
+ this.visitExpression(call.callee);
501
+ // Validate argument types if callee is a known function
502
+ if (call.callee.kind === AST.NodeType.Identifier) {
503
+ const funcName = call.callee.name;
504
+ const funcInfo = this.functionRegistry.get(funcName);
505
+ if (funcInfo) {
506
+ // Check each argument against expected param type
507
+ for (let i = 0; i < call.args.length && i < funcInfo.params.length; i++) {
508
+ const expectedType = funcInfo.params[i].type;
509
+ const argType = this.inferType(call.args[i]);
510
+ if (expectedType !== 'any' && argType !== 'any' && expectedType !== argType) {
511
+ throw new Error(`Type mismatch in function call '${funcName}' at line ${call.line || 0}: Argument ${i + 1} expects '${expectedType}', got '${argType}'`);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ for (const arg of call.args) {
517
+ this.visitExpression(arg);
518
+ }
519
+ return;
520
+ }
521
+ // Handle MemberExpr - only check the object, not property (obj.property)
522
+ if (expr.kind === AST.NodeType.MemberExpr) {
523
+ const member = expr;
524
+ this.visitExpression(member.object);
525
+ // Don't check member.property - it's a field access, not a variable
526
+ return;
527
+ }
340
528
  }
341
529
  inferType(expr) {
342
530
  if (!expr)
@@ -361,29 +549,96 @@ export class SemanticAnalyzer {
361
549
  }
362
550
  }
363
551
  stringToType(str) {
364
- switch (str) {
552
+ // Array types are tables
553
+ if (str.endsWith('[]'))
554
+ return 'table';
555
+ // Remove array notation for basic type check: string[] -> string
556
+ const baseStr = str.replace(/\[\]/g, '');
557
+ switch (baseStr) {
365
558
  case 'string': return 'string';
366
559
  case 'number': return 'number';
367
560
  case 'boolean': return 'boolean';
368
- case 'vector': return 'vector';
561
+ case 'vector':
562
+ case 'vector2':
563
+ case 'vector3':
564
+ case 'vector4': return 'vector';
369
565
  case 'table': return 'table';
370
566
  case 'function': return 'function';
371
567
  case 'any': return 'any';
372
568
  default:
373
- // Check if it's a custom type
374
- if (this.typeRegistry.has(str)) {
375
- return 'table'; // Custom types are tables internally
569
+ // Check if it's a registered interface
570
+ if (this.typeRegistry.has(baseStr)) {
571
+ return 'table';
572
+ }
573
+ // Check if it's a type alias
574
+ if (this.typeAliasRegistry.has(baseStr)) {
575
+ // If alias resolves to a primitive, return that. If object literal, return 'table'
576
+ const aliased = this.typeAliasRegistry.get(baseStr);
577
+ if (aliased.startsWith('{'))
578
+ return 'table';
579
+ return this.stringToType(aliased);
376
580
  }
377
581
  return 'any';
378
582
  }
379
583
  }
584
+ visitTypeAliasDecl(decl) {
585
+ this.typeAliasRegistry.set(decl.name.name, decl.type);
586
+ // If it's an object literal type, validate field types
587
+ if (decl.type.startsWith('{')) {
588
+ const fields = this.parseObjectLiteralType(decl.type);
589
+ for (const field of fields) {
590
+ this.validateTypeExists(field.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
591
+ }
592
+ }
593
+ else {
594
+ // It's a simple alias, validate the aliased type exists
595
+ this.validateTypeExists(decl.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
596
+ }
597
+ }
380
598
  visitTypeDecl(decl) {
381
599
  const typeName = decl.name.name;
382
600
  // Register the type
383
601
  this.typeRegistry.set(typeName, decl.fields);
602
+ // Validate that all field types exist
603
+ for (const field of decl.fields) {
604
+ this.validateTypeExists(field.type, decl.name.line ?? 0, `interface '${typeName}'`);
605
+ }
606
+ }
607
+ validateTypeExists(typeStr, line, context) {
608
+ // Remove array brackets for base type check
609
+ const baseType = typeStr.replace(/\[\]/g, '').trim();
610
+ // Skip object literal types (they are inline definitions)
611
+ if (baseType.startsWith('{'))
612
+ return;
613
+ // Check primitive types (including FiveM vector types)
614
+ const primitives = ['string', 'number', 'boolean', 'nil', 'any', 'table', 'function', 'vector', 'void', 'vector2', 'vector3', 'vector4'];
615
+ if (primitives.includes(baseType))
616
+ return;
617
+ // Check type registry (interfaces)
618
+ if (this.typeRegistry.has(baseType))
619
+ return;
620
+ // Check type alias registry
621
+ if (this.typeAliasRegistry.has(baseType))
622
+ return;
623
+ // Type not found!
624
+ throw new Error(`Unknown type '${baseType}' in ${context} at line ${line}`);
384
625
  }
385
626
  validateTableAgainstType(table, typeName, line) {
386
- const fields = this.typeRegistry.get(typeName);
627
+ let fields;
628
+ // Remove array brackets if present (e.g. validating a single object against MyType[])
629
+ // This shouldn't happen often in this function call context but good for safety
630
+ typeName = typeName.replace(/\[\]/g, '');
631
+ // 1. Check Interface Registry
632
+ if (this.typeRegistry.has(typeName)) {
633
+ fields = this.typeRegistry.get(typeName);
634
+ }
635
+ // 2. Check Type Aliases (specifically object literal aliases)
636
+ else if (this.typeAliasRegistry.has(typeName)) {
637
+ const aliasStr = this.typeAliasRegistry.get(typeName);
638
+ if (aliasStr.startsWith('{')) {
639
+ fields = this.parseObjectLiteralType(aliasStr);
640
+ }
641
+ }
387
642
  if (!fields)
388
643
  return; // Unknown type, skip validation
389
644
  const providedFields = new Map();
@@ -408,4 +663,20 @@ export class SemanticAnalyzer {
408
663
  }
409
664
  }
410
665
  }
666
+ parseObjectLiteralType(typeStr) {
667
+ const content = typeStr.trim().slice(1, -1);
668
+ if (!content)
669
+ return [];
670
+ const fields = [];
671
+ const parts = content.split(/,|;/);
672
+ for (const part of parts) {
673
+ if (!part.trim())
674
+ continue;
675
+ const [key, val] = part.split(':').map(s => s.trim());
676
+ if (key && val) {
677
+ fields.push({ name: key.replace('?', ''), type: val });
678
+ }
679
+ }
680
+ return fields;
681
+ }
411
682
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lt-script",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "LT Language Compiler for FiveM",
6
6
  "main": "dist/index.js",