@tilua-types/roblox 1.0.0 → 2.1.0
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 +25 -0
- package/lowering.mjs +84 -0
- package/package.json +6 -3
- package/runtime/roblox.luau +77 -0
package/README.md
CHANGED
|
@@ -43,6 +43,31 @@ tree, down to `script.Parent`.
|
|
|
43
43
|
Deprecated members are left out. Deprecated globals such as `wait` and
|
|
44
44
|
`spawn` stay.
|
|
45
45
|
|
|
46
|
+
## Errors in callbacks
|
|
47
|
+
|
|
48
|
+
A bundle's entry runs under `xpcall`, so an uncaught error names the project's
|
|
49
|
+
files, not lines of the bundle. A callback that Roblox runs later on its own
|
|
50
|
+
thread is outside that `xpcall`. This package's lowering (`lowering.mjs`) wraps
|
|
51
|
+
those callbacks in the same handler:
|
|
52
|
+
|
|
53
|
+
- `signal:Connect(f)`, `signal:Once(f)`, `signal:ConnectParallel(f)`
|
|
54
|
+
- `task.spawn`, `task.defer`, `task.delay`, including `const spawn = task.spawn`
|
|
55
|
+
- `RunService:BindToRenderStep`, `game:BindToClose`,
|
|
56
|
+
`ContextActionService:BindAction` and `BindActionAtPriority`
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
main.tilua:5: attempt to index nil with 'Name'
|
|
60
|
+
at main.tilua:5 (touched)
|
|
61
|
+
at main.tilua:7
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The error is still raised on the callback's thread, so the callback stops the
|
|
65
|
+
same way it did before. A callback that runs synchronously (a `table.sort`
|
|
66
|
+
comparator, `names:map(f)`) is not wrapped, because it is already inside its
|
|
67
|
+
caller's `xpcall`. `coroutine` is not wrapped either: `coroutine.resume`
|
|
68
|
+
returning `false, err` is part of how it behaves. Outside a bundle, nothing is
|
|
69
|
+
wrapped.
|
|
70
|
+
|
|
46
71
|
## Where it comes from
|
|
47
72
|
|
|
48
73
|
`index.d.tilua` is generated. Don't edit it by hand; change
|
package/lowering.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* What tilua lowers for Roblox: callbacks that Roblox runs later, on a thread
|
|
4
|
+
* of their own.
|
|
5
|
+
*
|
|
6
|
+
* The entry of a bundle runs under `xpcall`, which is what reports an error
|
|
7
|
+
* in the project's files instead of in the bundle's lines. A callback given
|
|
8
|
+
* to `task.spawn` or to `signal:Connect` is not under it when it runs, so its
|
|
9
|
+
* callback is wrapped in the same handler first (see runtime/roblox.luau):
|
|
10
|
+
*
|
|
11
|
+
* part.Touched:Connect(onTouched) -> tilua_roblox.Connect(part.Touched, onTouched)
|
|
12
|
+
* task.spawn(work, 1) -> tilua_roblox.task.spawn(work, 1)
|
|
13
|
+
*
|
|
14
|
+
* Only where Roblox starts a thread. A callback run synchronously — a
|
|
15
|
+
* `table.sort` comparator, `names:map(f)` — has its caller's `xpcall` already,
|
|
16
|
+
* and `coroutine` is left alone: `coroutine.resume` answering `false, err` is
|
|
17
|
+
* part of what it promises.
|
|
18
|
+
*
|
|
19
|
+
* `print`, `error` and the console come from `@tilua-types/lua`, which this
|
|
20
|
+
* package depends on.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync } from "node:fs"
|
|
23
|
+
import { dirname, join } from "node:path"
|
|
24
|
+
import { fileURLToPath } from "node:url"
|
|
25
|
+
|
|
26
|
+
/** @typedef {import("@tilua/parser").LoweringPlugin} LoweringPlugin */
|
|
27
|
+
/** @typedef {import("@tilua/parser").Type} Type */
|
|
28
|
+
|
|
29
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
30
|
+
const roblox = readFileSync(join(here, "runtime", "roblox.luau"), "utf8")
|
|
31
|
+
|
|
32
|
+
const SIGNAL_METHODS = new Set(["Connect", "Once", "ConnectParallel"])
|
|
33
|
+
|
|
34
|
+
/** A service's methods that keep a callback, by the class that has them. */
|
|
35
|
+
const SERVICE_METHODS = new Map([
|
|
36
|
+
["BindToRenderStep", "RunService"],
|
|
37
|
+
["BindToClose", "DataModel"],
|
|
38
|
+
["BindAction", "ContextActionService"],
|
|
39
|
+
["BindActionAtPriority", "ContextActionService"],
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
/** The members of a type that may be nil: a check the analyzer asked for
|
|
43
|
+
* elsewhere, not a reason to lower differently here.
|
|
44
|
+
* @type {(type: Type | undefined) => Type[]} */
|
|
45
|
+
function parts(type) {
|
|
46
|
+
if (!type) return []
|
|
47
|
+
if (type.kind !== "union") return [type]
|
|
48
|
+
return type.types.filter(m => !(m.kind === "primitive" && m.name === "nil"))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `RBXScriptSignal<T...>` is structural once it is resolved: an object that
|
|
52
|
+
* can be connected to and waited on.
|
|
53
|
+
* @type {(type: Type | undefined) => boolean} */
|
|
54
|
+
function isSignal(type) {
|
|
55
|
+
const members = parts(type)
|
|
56
|
+
return members.length > 0 && members.every(m =>
|
|
57
|
+
m.kind === "object" && m.properties.has("Connect") && m.properties.has("Once") && m.properties.has("Wait"))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @type {(type: Type | undefined, className: string) => boolean} */
|
|
61
|
+
function isInstanceOf(type, className) {
|
|
62
|
+
const members = parts(type)
|
|
63
|
+
return members.length > 0 && members.every(m =>
|
|
64
|
+
m.kind === "object" && m.class !== undefined && m.class.ancestors.includes(className))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @type {LoweringPlugin} */
|
|
68
|
+
const plugin = {
|
|
69
|
+
runtime: { roblox },
|
|
70
|
+
|
|
71
|
+
methodCall({ method, receiver, use }) {
|
|
72
|
+
if (SIGNAL_METHODS.has(method) && isSignal(receiver)) return { callee: `${use("roblox")}.${method}` }
|
|
73
|
+
const service = SERVICE_METHODS.get(method)
|
|
74
|
+
if (service !== undefined && isInstanceOf(receiver, service)) return { callee: `${use("roblox")}.${method}` }
|
|
75
|
+
return undefined
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
globalValue({ name, use }) {
|
|
79
|
+
if (name === "task") return `${use("roblox")}.task`
|
|
80
|
+
return undefined
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default plugin
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tilua-types/roblox",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Luau and the whole Roblox API as tilua types: every class, enum, data type, service and global",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"files": [
|
|
7
7
|
"index.d.tilua",
|
|
8
|
+
"lowering.mjs",
|
|
9
|
+
"runtime",
|
|
8
10
|
"README.md",
|
|
9
11
|
"THIRD_PARTY_NOTICES.md"
|
|
10
12
|
],
|
|
11
13
|
"tilua": {
|
|
12
|
-
"types": "index.d.tilua"
|
|
14
|
+
"types": "index.d.tilua",
|
|
15
|
+
"lowering": "lowering.mjs"
|
|
13
16
|
},
|
|
14
17
|
"dependencies": {
|
|
15
|
-
"@tilua-types/lua": "^1.0
|
|
18
|
+
"@tilua-types/lua": "^2.1.0"
|
|
16
19
|
},
|
|
17
20
|
"keywords": [
|
|
18
21
|
"tilua",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
-- Callbacks Roblox runs on a thread of their own, reported in the project's
|
|
2
|
+
-- files.
|
|
3
|
+
--
|
|
4
|
+
-- A bundle's entry runs under `xpcall`, so an error nothing caught names the
|
|
5
|
+
-- file and line the author wrote. A callback handed to `task.spawn` or to
|
|
6
|
+
-- `signal:Connect` runs later, on a new thread, where that `xpcall` is not on
|
|
7
|
+
-- the stack: its errors would name lines of the bundle. `guard` gives such a
|
|
8
|
+
-- callback the same handler — `__FAIL__`, the bundle's own — and raises what
|
|
9
|
+
-- it returns on the callback's thread, where Roblox reports it as before.
|
|
10
|
+
--
|
|
11
|
+
-- The error is raised again, never swallowed: code that relied on it
|
|
12
|
+
-- stopping the callback still sees it stop. Outside a bundle there is nothing
|
|
13
|
+
-- to map, and callbacks are passed through untouched.
|
|
14
|
+
|
|
15
|
+
local __NAME__ = {}
|
|
16
|
+
local fail = __FAIL__
|
|
17
|
+
|
|
18
|
+
local function guard(callback)
|
|
19
|
+
if type(callback) ~= "function" or fail == nil then
|
|
20
|
+
return callback
|
|
21
|
+
end
|
|
22
|
+
return function(...)
|
|
23
|
+
local results = table.pack(xpcall(callback, fail, ...))
|
|
24
|
+
if not results[1] then
|
|
25
|
+
error(results[2], 0)
|
|
26
|
+
end
|
|
27
|
+
return table.unpack(results, 2, results.n)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
__NAME__.guard = guard
|
|
32
|
+
|
|
33
|
+
-- `signal:Connect(f)` and its siblings.
|
|
34
|
+
function __NAME__.Connect(signal, callback)
|
|
35
|
+
return signal:Connect(guard(callback))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
function __NAME__.Once(signal, callback)
|
|
39
|
+
return signal:Once(guard(callback))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
function __NAME__.ConnectParallel(signal, callback)
|
|
43
|
+
return signal:ConnectParallel(guard(callback))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
-- Services that keep a callback and run it later.
|
|
47
|
+
function __NAME__.BindToRenderStep(service, name, priority, callback)
|
|
48
|
+
return service:BindToRenderStep(name, priority, guard(callback))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
function __NAME__.BindToClose(game, callback)
|
|
52
|
+
return game:BindToClose(guard(callback))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
function __NAME__.BindAction(service, name, callback, ...)
|
|
56
|
+
return service:BindAction(name, guard(callback), ...)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
function __NAME__.BindActionAtPriority(service, name, callback, ...)
|
|
60
|
+
return service:BindActionAtPriority(name, guard(callback), ...)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
-- `task`, with the functions that start a thread guarding what they start.
|
|
64
|
+
-- A thread passed instead of a function is left as it is. Everything else is
|
|
65
|
+
-- `task`'s own. Standing in for the global, rather than rewriting calls,
|
|
66
|
+
-- covers `local spawn = task.spawn` too.
|
|
67
|
+
__NAME__.task = setmetatable({
|
|
68
|
+
spawn = function(target, ...)
|
|
69
|
+
return task.spawn(guard(target), ...)
|
|
70
|
+
end,
|
|
71
|
+
defer = function(target, ...)
|
|
72
|
+
return task.defer(guard(target), ...)
|
|
73
|
+
end,
|
|
74
|
+
delay = function(duration, target, ...)
|
|
75
|
+
return task.delay(duration, guard(target), ...)
|
|
76
|
+
end,
|
|
77
|
+
}, { __index = task })
|