@tilua-types/luau 8.0.0 → 9.0.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 +67 -37
- package/exports.mjs +311 -0
- package/lowering.mjs +12 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,37 +1,67 @@
|
|
|
1
|
-
# @tilua-types/luau
|
|
2
|
-
|
|
3
|
-
Luau as a tilua type library.
|
|
4
|
-
|
|
5
|
-
tilua itself knows Lua: its standard library is Lua's, and what the compiler
|
|
6
|
-
writes runs on Lua 5.1 to 5.4, LuaJIT and Luau alike. This library is what
|
|
7
|
-
makes a project Luau:
|
|
8
|
-
|
|
9
|
-
- **Luau's additions, typed.** `buffer`, `bit32`, `utf8`, `string.split`,
|
|
10
|
-
`table.find`, `table.freeze`, `warn`, `debug.info` and the rest — added to
|
|
11
|
-
the standard library's own tables rather than replacing them.
|
|
12
|
-
- **Luau's syntax in the output.** The compiler writes `a += b`, `continue`,
|
|
13
|
-
`if c then a else b`, `a // b` and `for _, v in t` as they are, where plain
|
|
14
|
-
Lua needs longer forms: shorter output, and the code Luau compiles fastest.
|
|
15
|
-
A bundle of several files loads the others with `require` by path.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
# @tilua-types/luau
|
|
2
|
+
|
|
3
|
+
Luau as a tilua type library.
|
|
4
|
+
|
|
5
|
+
tilua itself knows Lua: its standard library is Lua's, and what the compiler
|
|
6
|
+
writes runs on Lua 5.1 to 5.4, LuaJIT and Luau alike. This library is what
|
|
7
|
+
makes a project Luau:
|
|
8
|
+
|
|
9
|
+
- **Luau's additions, typed.** `buffer`, `bit32`, `utf8`, `string.split`,
|
|
10
|
+
`table.find`, `table.freeze`, `warn`, `debug.info` and the rest — added to
|
|
11
|
+
the standard library's own tables rather than replacing them.
|
|
12
|
+
- **Luau's syntax in the output.** The compiler writes `a += b`, `continue`,
|
|
13
|
+
`if c then a else b`, `a // b` and `for _, v in t` as they are, where plain
|
|
14
|
+
Lua needs longer forms: shorter output, and the code Luau compiles fastest.
|
|
15
|
+
A bundle of several files loads the others with `require` by path.
|
|
16
|
+
- **Typed exports.** A bundle whose entry exports is cast to the entry's
|
|
17
|
+
exports, written as Luau types, so a script that `require`s it knows what
|
|
18
|
+
it got (see below).
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm i -D @tilua-types/luau
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{ "types": ["luau"] }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
[`@tilua-types/roblox`](../roblox) loads it: Roblox's Lua is Luau, so a
|
|
29
|
+
Roblox project has both without naming this one.
|
|
30
|
+
|
|
31
|
+
## Typed exports
|
|
32
|
+
|
|
33
|
+
```lua
|
|
34
|
+
export type Point = { x: number, y: number }
|
|
35
|
+
export type Dog = { name: string, friend: (self: Dog) -> Dog, ClassObject: DogClass }
|
|
36
|
+
type DogClass = { new: (name: string) -> Dog, name: "Dog", ParentClass: nil }
|
|
37
|
+
return (G.require("src/main") :: any) :: { origin: () -> Point, Dog: DogClass, id: <T>(v: T) -> T }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Every `export type` of the entry is written as one, so the requirer can
|
|
41
|
+
name it: `local p: Main.Point = Main.origin()`.
|
|
42
|
+
- Any other named type the exports reach — a class, an interface — gets a
|
|
43
|
+
local alias, which is also how a type that reaches itself is written.
|
|
44
|
+
- A class the host made (a Roblox `Part`, `Vector3`) is named as it is: its
|
|
45
|
+
Luau has that type already.
|
|
46
|
+
- What Luau's types cannot say is written as near as they come: a tuple is a
|
|
47
|
+
table of its elements' union, a template literal type a `string`, and a
|
|
48
|
+
conditional or mapped type that was never resolved `any`. A `private`
|
|
49
|
+
member is left out.
|
|
50
|
+
|
|
51
|
+
The cast goes through `any`, so it is never an error in the bundle itself.
|
|
52
|
+
It is this library's option, on unless the config turns it off:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{ "types": ["luau"], "typedExports": false }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
<!-- compatibility:start -->
|
|
59
|
+
## Compatibility
|
|
60
|
+
|
|
61
|
+
`@tilua-types/luau@9.0.0` works with these tilua packages. *Released with* is the
|
|
62
|
+
version it was built and tested against.
|
|
63
|
+
|
|
64
|
+
| Package | Range | Released with | |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `@tilua/parser` | `^9.0.0` | 9.0.0 | peer |
|
|
67
|
+
<!-- compatibility:end -->
|
package/exports.mjs
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* What the entry exports, written as a Luau type.
|
|
4
|
+
*
|
|
5
|
+
* A bundle that exports is a ModuleScript, and the script that `require`s it
|
|
6
|
+
* reads its type from what it returns. Lowered, that is `G.require(...)` — a
|
|
7
|
+
* table nothing types — so the bundle casts it on the way out:
|
|
8
|
+
*
|
|
9
|
+
* export type Point = { x: number, y: number }
|
|
10
|
+
* type DogClass = { new: (name: string) -> Dog, ... }
|
|
11
|
+
* export type Dog = { name: string, friend: (self: Dog) -> Dog, ... }
|
|
12
|
+
* return (G.require("src/main") :: any) :: { origin: () -> Point, Dog: DogClass }
|
|
13
|
+
*
|
|
14
|
+
* The cast goes through `any`, so it can never be an error in the bundle
|
|
15
|
+
* itself. Every `export type` of the entry is written as one, so a requirer
|
|
16
|
+
* can name it (`Main.Point`). Any other named type the exports reach — a
|
|
17
|
+
* class, an interface — gets a local alias, which is also what lets a type
|
|
18
|
+
* that reaches itself (`friend(): Dog`) be written at all. A class the host
|
|
19
|
+
* made (a Roblox `Part`) is named as it is: its Luau has that type already.
|
|
20
|
+
*
|
|
21
|
+
* It is the reader's view, not a proof: what Luau's types cannot say — a
|
|
22
|
+
* tuple, a conditional, a mapped type — is written as near as Luau comes,
|
|
23
|
+
* and as `any` where nothing comes near.
|
|
24
|
+
*/
|
|
25
|
+
import { isIdentifier, isLuauName, luauString } from "@tilua/parser"
|
|
26
|
+
|
|
27
|
+
/** @typedef {import("@tilua/parser").Type} Type */
|
|
28
|
+
/** @typedef {import("@tilua/parser").ModuleExports} ModuleExports */
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} Alias
|
|
31
|
+
* @property {string} name
|
|
32
|
+
* @property {Type} type
|
|
33
|
+
* @property {readonly string[]} params
|
|
34
|
+
* @property {string} shape its property names, to tell two types of one name apart
|
|
35
|
+
* @property {string} [body]
|
|
36
|
+
* @property {boolean} [exported]
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** How deep an unnamed type is written before the rest is `any`. */
|
|
40
|
+
const MAX_DEPTH = 12
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The type of `exports`' table, and the aliases it names — or `undefined`
|
|
44
|
+
* when the module's exports are not known.
|
|
45
|
+
* @param {ModuleExports} exports
|
|
46
|
+
* @returns {{ declarations: string[], type: string } | undefined}
|
|
47
|
+
*/
|
|
48
|
+
export function typedExports(exports) {
|
|
49
|
+
if (exports.partial) return undefined
|
|
50
|
+
const writer = new TypeWriter()
|
|
51
|
+
// The module's own `export type`s first: they keep the names they have.
|
|
52
|
+
// `default` is the type of an `export default class`, which the class's
|
|
53
|
+
// own name already says.
|
|
54
|
+
const named = [...exports.types].filter(([name]) => isLuauName(name) && name !== "default")
|
|
55
|
+
for (const [name, exported] of named) writer.reserve(name, exported.type, exported.params)
|
|
56
|
+
/** @type {[string, Type][]} */
|
|
57
|
+
const fields = [...exports.values]
|
|
58
|
+
if (exports.default) fields.push(["default", exports.default])
|
|
59
|
+
const type = writer.table(fields.map(([name, type]) => ({ name, type, optional: false })), undefined, new Set(), new Set(), 0)
|
|
60
|
+
for (const [name, exported] of named) writer.define(name, exported.type, exported.params)
|
|
61
|
+
return { declarations: writer.declarations(), type }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class TypeWriter {
|
|
65
|
+
constructor() {
|
|
66
|
+
/** @type {Map<Type, Alias>} */
|
|
67
|
+
this.byType = new Map()
|
|
68
|
+
/** @type {Map<string, Alias>} */
|
|
69
|
+
this.byName = new Map()
|
|
70
|
+
/** Aliases by what the program called them, every one of that name.
|
|
71
|
+
* @type {Map<string, Alias[]>} */
|
|
72
|
+
this.written = new Map()
|
|
73
|
+
/** @type {Alias[]} */
|
|
74
|
+
this.order = []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Claim `name` for `type` before anything is written, so that the
|
|
78
|
+
* program's own name is the one it keeps.
|
|
79
|
+
* @param {string} name @param {Type} type @param {readonly string[]} params */
|
|
80
|
+
reserve(name, type, params) {
|
|
81
|
+
this.add(name, name, type, params).exported = true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Write the body of an alias `reserve` claimed.
|
|
85
|
+
* @param {string} name @param {Type} type @param {readonly string[]} params */
|
|
86
|
+
define(name, type, params) {
|
|
87
|
+
const alias = this.byName.get(name)
|
|
88
|
+
if (alias && alias.body === undefined) alias.body = this.write(type, new Set(params), new Set(), 0, true)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @returns {string[]} */
|
|
92
|
+
declarations() {
|
|
93
|
+
return this.order.filter(a => a.body !== undefined).map(a => {
|
|
94
|
+
const params = a.params.length ? `<${a.params.join(", ")}>` : ""
|
|
95
|
+
return `${a.exported ? "export " : ""}type ${a.name}${params} = ${a.body}`
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @param {string} given @param {string} name @param {Type} type @param {readonly string[]} params */
|
|
100
|
+
add(given, name, type, params) {
|
|
101
|
+
/** @type {Alias} */
|
|
102
|
+
const alias = { name, type, params, shape: shapeOf(type) }
|
|
103
|
+
// Two names for one type: the first keeps it.
|
|
104
|
+
if (!this.byType.has(type)) this.byType.set(type, alias)
|
|
105
|
+
this.byName.set(name, alias)
|
|
106
|
+
const same = this.written.get(given) ?? []
|
|
107
|
+
same.push(alias)
|
|
108
|
+
this.written.set(given, same)
|
|
109
|
+
this.order.push(alias)
|
|
110
|
+
return alias
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The alias that writes the named type `type` — the program's name for
|
|
114
|
+
* it, or a fresh one where that is taken by a different type.
|
|
115
|
+
* @param {string} given @param {Type} type @param {ReadonlySet<string>} scope */
|
|
116
|
+
named(given, type, scope) {
|
|
117
|
+
const known = [this.byType.get(type), ...(this.written.get(given) ?? [])]
|
|
118
|
+
.find(a => a !== undefined && a.params.length === 0 && (a.type === type || a.shape === shapeOf(type)))
|
|
119
|
+
if (known) return known.name
|
|
120
|
+
const base = luauTypeName(given)
|
|
121
|
+
let name = base
|
|
122
|
+
for (let n = 2; this.byName.has(name) || scope.has(name); n++) name = `${base}${n}`
|
|
123
|
+
const alias = this.add(given, name, type, [])
|
|
124
|
+
// Registered before it is written: a type that reaches itself names
|
|
125
|
+
// the alias on the way back instead of writing itself forever.
|
|
126
|
+
alias.body = this.write(type, new Set(), new Set(), 0, true)
|
|
127
|
+
return name
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `type` in Luau. `scope` holds the type parameters that may be named
|
|
132
|
+
* here; `seen`, the unnamed types being written further up. `body` says
|
|
133
|
+
* this is the alias's own body, which is written out, not named.
|
|
134
|
+
* @param {Type} type @param {ReadonlySet<string>} scope @param {ReadonlySet<Type>} seen
|
|
135
|
+
* @param {number} depth @param {boolean} [body]
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
write(type, scope, seen, depth, body = false) {
|
|
139
|
+
// A type an alias already writes is named, not written again — even
|
|
140
|
+
// inside its own alias, which is how a type reaches itself.
|
|
141
|
+
const alias = body ? undefined : this.byType.get(type)
|
|
142
|
+
if (alias && alias.params.length === 0) return alias.name
|
|
143
|
+
if (depth > MAX_DEPTH || seen.has(type)) return "any"
|
|
144
|
+
const inner = new Set(seen).add(type)
|
|
145
|
+
/** @param {Type} t @param {ReadonlySet<string>} [s] */
|
|
146
|
+
const next = (t, s = scope) => this.write(t, s, inner, depth + 1)
|
|
147
|
+
switch (type.kind) {
|
|
148
|
+
case "any": return "any"
|
|
149
|
+
case "unknown": return "unknown"
|
|
150
|
+
case "never": return "never"
|
|
151
|
+
case "primitive":
|
|
152
|
+
if (type.name === "void") return "nil"
|
|
153
|
+
return type.name === "object" ? "any" : type.name
|
|
154
|
+
case "literal":
|
|
155
|
+
if (type.base === "string") return luauString(String(type.value))
|
|
156
|
+
return type.base === "boolean" ? String(type.value) : "number"
|
|
157
|
+
case "array": return `{ ${next(type.element)} }`
|
|
158
|
+
case "tuple": {
|
|
159
|
+
// A table of the elements: Luau has no type for each place.
|
|
160
|
+
const members = [...type.elements, ...(type.rest ? [type.rest] : [])]
|
|
161
|
+
return members.length ? `{ ${union(members.map(m => next(m)))} }` : "{}"
|
|
162
|
+
}
|
|
163
|
+
case "object": {
|
|
164
|
+
if (!body) {
|
|
165
|
+
// `Box<number>`: an instance of a generic alias this file writes.
|
|
166
|
+
const ref = type.nameRef
|
|
167
|
+
const generic = ref && this.byName.get(ref.name)
|
|
168
|
+
if (ref && generic && generic.params.length === ref.typeArguments.length) {
|
|
169
|
+
return `${generic.name}<${ref.typeArguments.map(a => next(a)).join(", ")}>`
|
|
170
|
+
}
|
|
171
|
+
// A class the host made — a Roblox `Part` — is a type
|
|
172
|
+
// its Luau already has, by the same name.
|
|
173
|
+
if (type.class && !type.class.runtime && isLuauName(type.class.name)) {
|
|
174
|
+
const args = type.class.typeArguments?.get(type.class.name)
|
|
175
|
+
return args?.length ? `${type.class.name}<${args.map(a => next(a)).join(", ")}>` : type.class.name
|
|
176
|
+
}
|
|
177
|
+
if (type.name && !ref) return this.named(type.name, type, scope)
|
|
178
|
+
}
|
|
179
|
+
const properties = [...type.properties]
|
|
180
|
+
// What only the class itself may read is not the requirer's.
|
|
181
|
+
.filter(([, p]) => !p.private)
|
|
182
|
+
.map(([name, p]) => ({ name, type: p.type, optional: p.optional }))
|
|
183
|
+
return this.table(properties, type.indexer, scope, inner, depth)
|
|
184
|
+
}
|
|
185
|
+
case "function": {
|
|
186
|
+
const generic = new Set([...scope, ...(type.typeParams ?? [])])
|
|
187
|
+
const params = type.params.map((p, i) => {
|
|
188
|
+
const written = this.write(p.type, generic, inner, depth + 1)
|
|
189
|
+
const shown = p.optional ? optional(written) : written
|
|
190
|
+
// A method's receiver is Luau's `self`.
|
|
191
|
+
const name = i === 0 && (p.name === "this" || p.name === "self") ? "self" : p.name
|
|
192
|
+
return name && isLuauName(name) ? `${name}: ${shown}` : shown
|
|
193
|
+
})
|
|
194
|
+
if (type.varargs) {
|
|
195
|
+
params.push(type.restIsWhole ? "...any" : `...${this.write(type.varargs, generic, inner, depth + 1)}`)
|
|
196
|
+
}
|
|
197
|
+
const generics = type.typeParams?.length ? `<${type.typeParams.join(", ")}>` : ""
|
|
198
|
+
// Nothing, where Luau says so: `-> ()`, not one `nil`.
|
|
199
|
+
const returns = type.returns.kind === "primitive" && (type.returns.name === "void" || type.returns.name === "nil")
|
|
200
|
+
? "()" : atom(this.write(type.returns, generic, inner, depth + 1))
|
|
201
|
+
return `${generics}(${params.join(", ")}) -> ${returns}`
|
|
202
|
+
}
|
|
203
|
+
case "typeParam":
|
|
204
|
+
if (scope.has(type.name)) return type.name
|
|
205
|
+
return type.constraint ? next(type.constraint) : "any"
|
|
206
|
+
case "union": {
|
|
207
|
+
const members = type.types.map(t => next(t))
|
|
208
|
+
const hasNil = type.types.some(t => t.kind === "primitive" && (t.name === "nil" || t.name === "void"))
|
|
209
|
+
const rest = members.filter(m => m !== "nil")
|
|
210
|
+
if (!hasNil) return union(rest)
|
|
211
|
+
if (!rest.length) return "nil"
|
|
212
|
+
return rest.length === 1 ? optional(rest[0]) : `${union(rest)} | nil`
|
|
213
|
+
}
|
|
214
|
+
case "intersection": {
|
|
215
|
+
const members = [...new Set(type.types.map(t => atom(next(t))))]
|
|
216
|
+
return members.includes("any") ? "any" : members.join(" & ")
|
|
217
|
+
}
|
|
218
|
+
case "genericRef": {
|
|
219
|
+
const generic = this.byName.get(type.name)
|
|
220
|
+
if (generic && generic.params.length === type.typeArguments.length && generic.params.length > 0) {
|
|
221
|
+
return `${generic.name}<${type.typeArguments.map(a => next(a)).join(", ")}>`
|
|
222
|
+
}
|
|
223
|
+
if (scope.has(type.name) && !type.typeArguments.length) return type.name
|
|
224
|
+
const expanded = type.origin?.expand(type)
|
|
225
|
+
return expanded && expanded !== type ? next(expanded) : "any"
|
|
226
|
+
}
|
|
227
|
+
case "templateLiteral": return "string"
|
|
228
|
+
case "difference": return next(type.base)
|
|
229
|
+
default: return "any"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A table type: `{ name: T, ["not a name"]: U, [K]: V }`.
|
|
235
|
+
* @param {readonly { name: string, type: Type, optional: boolean }[]} properties
|
|
236
|
+
* @param {{ key: Type, value: Type } | undefined} indexer
|
|
237
|
+
* @param {ReadonlySet<string>} scope @param {ReadonlySet<Type>} seen @param {number} depth
|
|
238
|
+
*/
|
|
239
|
+
table(properties, indexer, scope, seen, depth) {
|
|
240
|
+
const fields = properties.map(p => {
|
|
241
|
+
const written = this.write(p.type, scope, seen, depth + 1)
|
|
242
|
+
const key = isLuauName(p.name) ? p.name : `[${luauString(p.name)}]`
|
|
243
|
+
return `${key}: ${p.optional ? optional(written) : written}`
|
|
244
|
+
})
|
|
245
|
+
if (indexer) fields.push(`[${this.write(indexer.key, scope, seen, depth + 1)}]: ${this.write(indexer.value, scope, seen, depth + 1)}`)
|
|
246
|
+
return fields.length ? `{ ${fields.join(", ")} }` : "{}"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** @param {readonly string[]} members */
|
|
251
|
+
function union(members) {
|
|
252
|
+
const unique = [...new Set(members)]
|
|
253
|
+
if (unique.includes("any")) return "any"
|
|
254
|
+
// `true | false` is how the analyzer holds a `boolean`.
|
|
255
|
+
if (unique.includes("true") && unique.includes("false")) {
|
|
256
|
+
unique.splice(unique.indexOf("true"), 1, "boolean")
|
|
257
|
+
unique.splice(unique.indexOf("false"), 1)
|
|
258
|
+
}
|
|
259
|
+
return unique.length ? unique.map(atom).join(" | ") : "never"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** `T?`, with `T` in parentheses where `?` would bind to less of it.
|
|
263
|
+
* @param {string} written */
|
|
264
|
+
function optional(written) {
|
|
265
|
+
if (written === "any" || written === "unknown" || written === "nil" || written.endsWith("?")) return written
|
|
266
|
+
if (written.endsWith(" | nil")) return written
|
|
267
|
+
return `${atom(written)}?`
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** `written` in parentheses where it is a function, a union or an
|
|
271
|
+
* intersection: an operand of `|`, `&` or `?`, or a function's result.
|
|
272
|
+
* @param {string} written */
|
|
273
|
+
function atom(written) {
|
|
274
|
+
return isCompound(written) ? `(${written})` : written
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Does `written` have `->`, `|` or `&` outside every bracket?
|
|
278
|
+
* @param {string} written */
|
|
279
|
+
function isCompound(written) {
|
|
280
|
+
let depth = 0
|
|
281
|
+
let quoted = false
|
|
282
|
+
for (let i = 0; i < written.length; i++) {
|
|
283
|
+
const c = written[i]
|
|
284
|
+
if (quoted) {
|
|
285
|
+
if (c === "\\") i++
|
|
286
|
+
else if (c === "\"") quoted = false
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
if (c === "\"") quoted = true
|
|
290
|
+
else if (c === "(" || c === "{" || c === "<") depth++
|
|
291
|
+
else if (c === ")" || c === "}" || (c === ">" && written[i - 1] !== "-")) depth--
|
|
292
|
+
else if (depth === 0 && (c === "|" || c === "&" || (c === "-" && written[i + 1] === ">"))) return true
|
|
293
|
+
}
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A type's property names: two types the program gave one name are one
|
|
298
|
+
* type only if they have the same members.
|
|
299
|
+
* @param {Type} type */
|
|
300
|
+
function shapeOf(type) {
|
|
301
|
+
return type.kind === "object" ? [...type.properties.keys()].sort().join(",") : type.kind
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** A name the program gave a type, as a Luau type name: `typeof Dog` — the
|
|
305
|
+
* class table — is `DogClass`.
|
|
306
|
+
* @param {string} given */
|
|
307
|
+
function luauTypeName(given) {
|
|
308
|
+
const name = given.startsWith("typeof ") ? `${given.slice("typeof ".length)}Class` : given
|
|
309
|
+
const cleaned = name.replace(/[^A-Za-z0-9_]/g, "_")
|
|
310
|
+
return isIdentifier(cleaned) && isLuauName(cleaned) ? cleaned : `T_${cleaned}`
|
|
311
|
+
}
|
package/lowering.mjs
CHANGED
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
* if c then a else b rather than a function called on the spot
|
|
11
11
|
* for _, v in t rather than ipairs(t), or a function doing what Luau does
|
|
12
12
|
* require("./chunk") rather than loadfile, which Luau has none of
|
|
13
|
+
*
|
|
14
|
+
* Luau has types, too: with `typedExports` (on unless the config turns it
|
|
15
|
+
* off), what the bundle's entry exports is cast to its type, so a script that
|
|
16
|
+
* `require`s the bundle knows what it got (see exports.mjs).
|
|
13
17
|
*/
|
|
18
|
+
import { typedExports } from "./exports.mjs"
|
|
14
19
|
|
|
15
20
|
/** @typedef {import("@tilua/parser").LoweringPlugin} LoweringPlugin */
|
|
16
21
|
|
|
@@ -26,6 +31,13 @@ const plugin = {
|
|
|
26
31
|
"numberLiterals",
|
|
27
32
|
"requireByPath",
|
|
28
33
|
],
|
|
34
|
+
entryExports({ value, exports, options }) {
|
|
35
|
+
if (options.typedExports === false) return undefined
|
|
36
|
+
const typed = typedExports(exports)
|
|
37
|
+
if (!typed) return undefined
|
|
38
|
+
// Through `any`, so the cast is never an error in the bundle itself.
|
|
39
|
+
return { statements: typed.declarations, value: `(${value} :: any) :: ${typed.type}` }
|
|
40
|
+
},
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
export default plugin
|
package/package.json
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tilua-types/luau",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Luau as a tilua type library: what Luau adds to Lua 5.1 (buffer, bit32, string.split, ...), and Luau's syntax in the compiled output",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"files": [
|
|
7
7
|
"index.d.tilua",
|
|
8
8
|
"lowering.mjs",
|
|
9
|
+
"exports.mjs",
|
|
9
10
|
"README.md"
|
|
10
11
|
],
|
|
11
12
|
"tilua": {
|
|
12
13
|
"types": "index.d.tilua",
|
|
13
|
-
"lowering": "lowering.mjs"
|
|
14
|
+
"lowering": "lowering.mjs",
|
|
15
|
+
"options": {
|
|
16
|
+
"typedExports": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"default": true,
|
|
19
|
+
"description": "Cast what the bundle's entry exports to its Luau type, so a script that requires the bundle knows what it got."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
14
22
|
},
|
|
15
23
|
"peerDependencies": {
|
|
16
|
-
"@tilua/parser": "^
|
|
24
|
+
"@tilua/parser": "^9.0.0"
|
|
17
25
|
},
|
|
18
26
|
"keywords": [
|
|
19
27
|
"tilua",
|