@tilua-types/roblox 9.0.0 → 10.0.1
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 +71 -3
- package/lowering.mjs +14 -0
- package/package.json +12 -4
- package/rojo.mjs +133 -0
package/README.md
CHANGED
|
@@ -71,6 +71,74 @@ caller's `xpcall`. `coroutine` is not wrapped either: `coroutine.resume`
|
|
|
71
71
|
returning `false, err` is part of how it behaves. Outside a bundle, nothing is
|
|
72
72
|
wrapped.
|
|
73
73
|
|
|
74
|
+
## A game through Rojo
|
|
75
|
+
|
|
76
|
+
In a Rojo project as `rojo init` makes it:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm i -D @tilua/compiler @tilua-types/roblox
|
|
80
|
+
npx tilua init # src is what you write, out what Rojo reads
|
|
81
|
+
npx tilua --watch # beside `rojo serve`
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
src/ what you write
|
|
86
|
+
shared/Greet.tilua
|
|
87
|
+
shared/Hello.luau Luau written by hand
|
|
88
|
+
out/ the build's: compiled, copied, and nothing else
|
|
89
|
+
shared/Greet.luau
|
|
90
|
+
shared/Hello.luau
|
|
91
|
+
shared/_tilua.luau the runtime
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`tilua init` writes `{ "types": ["roblox"], "output": "files", "rootDir": "src", "outDir": "out" }`
|
|
95
|
+
— `src` being the folder the project's `$path`s share — points those `$path`s
|
|
96
|
+
at `out`, and adds `/out` to `.gitignore`. Each build makes `out` a mirror of
|
|
97
|
+
`src`: every `.tilua` compiled, every other file (Luau written by hand,
|
|
98
|
+
`.meta.json`, models) copied as it is, every folder there even when empty,
|
|
99
|
+
and nothing else — what is in `out` and no longer in `src` is removed.
|
|
100
|
+
`--watch` does that again on every change under `src`, and `rojo serve`
|
|
101
|
+
syncs it into Studio. A project set up the older way, each `.luau` compiled
|
|
102
|
+
beside its `.tilua`, is moved over by `tilua init` the same way, and the first
|
|
103
|
+
build removes those `.luau` from `src`.
|
|
104
|
+
|
|
105
|
+
The tree the project builds is typed as you write, read straight off
|
|
106
|
+
`default.project.json` and the files in `src` — nothing else to run:
|
|
107
|
+
`game.Workspace.Baseplate` is the project's part, `script` is the file's own
|
|
108
|
+
instance (`script.Parent` its folder), and `FindFirstChild("")` and
|
|
109
|
+
`WaitForChild("")` offer the children's names.
|
|
110
|
+
|
|
111
|
+
Where each file goes is read off the Rojo project — `default.project.json`
|
|
112
|
+
beside the config, or the one `"rojo"` names — and the files require each
|
|
113
|
+
other by the paths the game knows them by:
|
|
114
|
+
|
|
115
|
+
```jsonc
|
|
116
|
+
// default.project.json, as `rojo init` writes it
|
|
117
|
+
{
|
|
118
|
+
"name": "game",
|
|
119
|
+
"tree": {
|
|
120
|
+
"$className": "DataModel",
|
|
121
|
+
"ReplicatedStorage": { "Shared": { "$path": "src/shared" } },
|
|
122
|
+
"ServerScriptService": { "Server": { "$path": "src/server" } },
|
|
123
|
+
"StarterPlayer": { "StarterPlayerScripts": { "Client": { "$path": "src/client" } } }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| in `src` | in the game | requires |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `server/main.server.tilua` | `ServerScriptService.Server.main`, a Script | `./data` beside it; `@game/ReplicatedStorage/Shared/net` across services |
|
|
131
|
+
| `client/hud.client.tilua` | a LocalScript under `StarterPlayerScripts` | waits for `game.Loaded` first: a `require` by path does not wait for what it names to replicate |
|
|
132
|
+
| `shared/net/init.tilua` | `ReplicatedStorage.Shared.net`, the folder as a ModuleScript | `@self/remote` below it, `./util` beside it |
|
|
133
|
+
|
|
134
|
+
- The runtime every file requires, `_tilua.luau`, goes in the folder the
|
|
135
|
+
project puts in `ReplicatedStorage`, where the server and the clients both
|
|
136
|
+
reach it. A project with none there is an error.
|
|
137
|
+
- `.server` and `.client` files are scripts the game runs, not modules:
|
|
138
|
+
importing one is an error, and what one returns is not typed.
|
|
139
|
+
- `import "./net"` finds `net/init.tilua`, as Rojo reads `net/init.luau`.
|
|
140
|
+
- A file the project puts nowhere is a warning: Rojo would leave it out.
|
|
141
|
+
|
|
74
142
|
## Where it comes from
|
|
75
143
|
|
|
76
144
|
`index.d.tilua` is generated. Don't edit it by hand; change
|
|
@@ -90,11 +158,11 @@ luau-lsp is MIT licensed. Its notice is in
|
|
|
90
158
|
<!-- compatibility:start -->
|
|
91
159
|
## Compatibility
|
|
92
160
|
|
|
93
|
-
`@tilua-types/roblox@
|
|
161
|
+
`@tilua-types/roblox@10.0.1` works with these tilua packages. *Released with* is the
|
|
94
162
|
version it was built and tested against.
|
|
95
163
|
|
|
96
164
|
| Package | Range | Released with | |
|
|
97
165
|
| --- | --- | --- | --- |
|
|
98
|
-
| `@tilua-types/luau` | `^
|
|
99
|
-
| `@tilua/parser` | `^
|
|
166
|
+
| `@tilua-types/luau` | `^9.0.1` | 9.0.1 | dependency |
|
|
167
|
+
| `@tilua/parser` | `^9.0.0` | 9.0.1 | peer |
|
|
100
168
|
<!-- compatibility:end -->
|
package/lowering.mjs
CHANGED
|
@@ -26,11 +26,17 @@
|
|
|
26
26
|
*
|
|
27
27
|
* `print`, `error` and the console are the compiler's own: Lua's standard
|
|
28
28
|
* library is built into tilua.
|
|
29
|
+
*
|
|
30
|
+
* With `"output": "files"`, where each file ends up in the game is read off
|
|
31
|
+
* the project's Rojo project (`rojo`, `default.project.json` unless set), and
|
|
32
|
+
* the files require each other by the paths the game knows them by (see
|
|
33
|
+
* rojo.mjs).
|
|
29
34
|
*/
|
|
30
35
|
import { readFileSync } from "node:fs"
|
|
31
36
|
import { dirname, join } from "node:path"
|
|
32
37
|
import { fileURLToPath } from "node:url"
|
|
33
38
|
import { hasMembers, isInstanceOf, luauString, returnsTuple } from "@tilua/parser"
|
|
39
|
+
import { rojoLayout } from "./rojo.mjs"
|
|
34
40
|
|
|
35
41
|
/** @typedef {import("@tilua/parser").LoweringPlugin} LoweringPlugin */
|
|
36
42
|
|
|
@@ -58,6 +64,9 @@ const SIGNAL_MEMBERS = ["Connect", "Once", "Wait"]
|
|
|
58
64
|
/** Luau globals answering several values, which the runtime gathers. */
|
|
59
65
|
const GATHERED_GLOBALS = new Set(["string.unpack", "coroutine.close", "utf8.codepoint", "utf8.codes", "utf8.graphemes"])
|
|
60
66
|
|
|
67
|
+
/** The Rojo project a config that names none has. */
|
|
68
|
+
const DEFAULT_PROJECT = "default.project.json"
|
|
69
|
+
|
|
61
70
|
/** @type {LoweringPlugin} */
|
|
62
71
|
const plugin = {
|
|
63
72
|
runtime: { roblox },
|
|
@@ -83,6 +92,11 @@ const plugin = {
|
|
|
83
92
|
if (GATHERED_GLOBALS.has(name)) return `${use("roblox")}.globals.${name.replace(".", "_")}`
|
|
84
93
|
return undefined
|
|
85
94
|
},
|
|
95
|
+
|
|
96
|
+
files(layout) {
|
|
97
|
+
const project = typeof layout.options.rojo === "string" ? layout.options.rojo : DEFAULT_PROJECT
|
|
98
|
+
return rojoLayout(layout, project, project !== DEFAULT_PROJECT)
|
|
99
|
+
},
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
export default plugin
|
package/package.json
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tilua-types/roblox",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.1",
|
|
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
8
|
"lowering.mjs",
|
|
9
|
+
"rojo.mjs",
|
|
9
10
|
"runtime",
|
|
10
11
|
"README.md",
|
|
11
12
|
"THIRD_PARTY_NOTICES.md"
|
|
12
13
|
],
|
|
13
14
|
"tilua": {
|
|
14
15
|
"types": "index.d.tilua",
|
|
15
|
-
"lowering": "lowering.mjs"
|
|
16
|
+
"lowering": "lowering.mjs",
|
|
17
|
+
"options": {
|
|
18
|
+
"rojo": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"default": "default.project.json",
|
|
21
|
+
"description": "With \"output\": \"files\", the Rojo project that says where each file goes in the game, from the config's folder."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
16
24
|
},
|
|
17
25
|
"dependencies": {
|
|
18
|
-
"@tilua-types/luau": "^
|
|
26
|
+
"@tilua-types/luau": "^9.0.1"
|
|
19
27
|
},
|
|
20
28
|
"peerDependencies": {
|
|
21
|
-
"@tilua/parser": "^
|
|
29
|
+
"@tilua/parser": "^9.0.0"
|
|
22
30
|
},
|
|
23
31
|
"keywords": [
|
|
24
32
|
"tilua",
|
package/rojo.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Where the files of `"output": "files"` end up in the game, read off a Rojo
|
|
4
|
+
* project, and how they reach each other there.
|
|
5
|
+
*
|
|
6
|
+
* A Rojo project maps folders to places in the game — `dist/shared` to
|
|
7
|
+
* `ReplicatedStorage.Shared`, `dist/server` to `ServerScriptService` — so a
|
|
8
|
+
* file's place is its folder's, and its own name below that:
|
|
9
|
+
*
|
|
10
|
+
* dist/shared/util.luau ReplicatedStorage.Shared.util a ModuleScript
|
|
11
|
+
* dist/server/main.server.luau ServerScriptService.main a Script
|
|
12
|
+
* dist/client/hud.client.luau StarterPlayer.StarterPlayerScripts.hud a LocalScript
|
|
13
|
+
* dist/shared/net/init.luau ReplicatedStorage.Shared.net the folder, as a ModuleScript
|
|
14
|
+
*
|
|
15
|
+
* Within one service a file requires another by the path between them
|
|
16
|
+
* (`./util`, `../net`); across services, from the top: `@game/ReplicatedStorage/Shared/util`.
|
|
17
|
+
* The runtime every file requires goes where both the server and the
|
|
18
|
+
* clients can reach it: the folder the project puts in ReplicatedStorage.
|
|
19
|
+
*
|
|
20
|
+
* A script the game runs (`.server`, `.client`) is not a module, so nothing
|
|
21
|
+
* may import one; a LocalScript waits for the game to load before it
|
|
22
|
+
* requires anything, since a `require` by path does not wait for what it
|
|
23
|
+
* names to replicate.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from "node:fs"
|
|
26
|
+
import { dirname, relative, resolve, sep } from "node:path"
|
|
27
|
+
|
|
28
|
+
/** `.server.luau` and the rest: a script the game runs, not a module. */
|
|
29
|
+
const SCRIPT = /\.(server|client|plugin|local|legacy)\.luau?$/
|
|
30
|
+
const CLIENT = /\.client\.luau?$/
|
|
31
|
+
|
|
32
|
+
/** Where the runtime goes, best first: somewhere every client reaches. */
|
|
33
|
+
const SHARED_SERVICES = ["ReplicatedStorage", "ReplicatedFirst"]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {{ folder: string, place: string[] }} Mapping
|
|
37
|
+
* A `$path` of the project — absolute — and the place in the game it fills.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The layout of the files of `layout` under the Rojo project `projectPath`
|
|
42
|
+
* (from the config's folder), or `undefined` when there is none to read.
|
|
43
|
+
* @param {import("@tilua/parser").FilesLayout} layout
|
|
44
|
+
* @param {string} projectPath
|
|
45
|
+
* @param {boolean} named whether the config named the project, rather than its default
|
|
46
|
+
* @returns {import("@tilua/parser").FilesAnswer}
|
|
47
|
+
*/
|
|
48
|
+
export function rojoLayout(layout, projectPath, named) {
|
|
49
|
+
const file = resolve(layout.directory, projectPath)
|
|
50
|
+
/** @type {import("@tilua/parser").FilesAnswer["problems"] & object[]} */
|
|
51
|
+
const problems = []
|
|
52
|
+
const script = (/** @type {string} */ path) => SCRIPT.test(path)
|
|
53
|
+
const prelude = (/** @type {string} */ path) => (CLIENT.test(path) ? "if not game:IsLoaded() then game.Loaded:Wait() end" : undefined)
|
|
54
|
+
|
|
55
|
+
let project
|
|
56
|
+
try {
|
|
57
|
+
project = JSON.parse(readFileSync(file, "utf8"))
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Without a project the files stay where they are written, side by side.
|
|
60
|
+
if (named) problems.push({ message: `Cannot read the Rojo project '${projectPath}': ${/** @type {Error} */ (error).message}` })
|
|
61
|
+
return { script, prelude, problems }
|
|
62
|
+
}
|
|
63
|
+
const tree = project && typeof project === "object" ? project.tree : undefined
|
|
64
|
+
if (!tree || typeof tree !== "object") {
|
|
65
|
+
problems.push({ file, message: "The Rojo project has no 'tree'" })
|
|
66
|
+
return { script, prelude, problems }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @type {Mapping[]} */
|
|
70
|
+
const mappings = []
|
|
71
|
+
const walk = (/** @type {Record<string, unknown>} */ node, /** @type {string[]} */ place) => {
|
|
72
|
+
const path = typeof node.$path === "string" ? node.$path
|
|
73
|
+
: node.$path && typeof node.$path === "object" ? /** @type {{ optional?: unknown }} */ (node.$path).optional : undefined
|
|
74
|
+
if (typeof path === "string") mappings.push({ folder: resolve(dirname(file), path), place })
|
|
75
|
+
for (const [name, child] of Object.entries(node)) {
|
|
76
|
+
if (!name.startsWith("$") && child && typeof child === "object") walk(/** @type {Record<string, unknown>} */ (child), [...place, name])
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walk(tree, [])
|
|
80
|
+
// The deepest mapping a file is under is the one that places it.
|
|
81
|
+
mappings.sort((a, b) => b.folder.length - a.folder.length)
|
|
82
|
+
|
|
83
|
+
/** @param {string} path */
|
|
84
|
+
const place = path => {
|
|
85
|
+
for (const mapping of mappings) {
|
|
86
|
+
const below = relative(mapping.folder, path)
|
|
87
|
+
if (below === "") return mapping.place
|
|
88
|
+
if (below.startsWith("..") || resolve(mapping.folder, below) !== resolve(path)) continue
|
|
89
|
+
const parts = below.split(sep)
|
|
90
|
+
const last = parts.pop() ?? ""
|
|
91
|
+
const name = last.replace(SCRIPT, "").replace(/\.luau?$/, "")
|
|
92
|
+
// `init.luau` is the folder it is in.
|
|
93
|
+
return name === "init" ? [...mapping.place, ...parts] : [...mapping.place, ...parts, name]
|
|
94
|
+
}
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const path of layout.files) {
|
|
99
|
+
if (!place(path)) {
|
|
100
|
+
problems.push({
|
|
101
|
+
file: path, severity: "warning",
|
|
102
|
+
message: `Nothing in the Rojo project '${projectPath}' puts this file in the game`,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The runtime: in the folder the project puts in a service every client
|
|
108
|
+
// reaches, so the server's files and the clients' all require one.
|
|
109
|
+
let runtime
|
|
110
|
+
for (const service of SHARED_SERVICES) {
|
|
111
|
+
const mapping = mappings.find(m => m.place[0] === service && isInside(layout.outDir, m.folder) && !/\.luau?$/.test(m.folder))
|
|
112
|
+
if (mapping) {
|
|
113
|
+
runtime = resolve(mapping.folder, "_tilua.luau")
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (runtime === undefined) {
|
|
118
|
+
problems.push({
|
|
119
|
+
message: `Nothing in the Rojo project '${projectPath}' puts a folder of outDir in ReplicatedStorage, `
|
|
120
|
+
+ "where the runtime every file requires has to be for the server and the clients to reach it",
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `@game` names the DataModel; a project that is not one is a single tree.
|
|
125
|
+
const root = tree.$className === "DataModel" ? "@game" : undefined
|
|
126
|
+
return { place, root, runtime, script, prelude, problems }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Is `path` `folder`, or under it? */
|
|
130
|
+
function isInside(/** @type {string} */ folder, /** @type {string} */ path) {
|
|
131
|
+
const below = relative(folder, path)
|
|
132
|
+
return below === "" || (!below.startsWith("..") && resolve(folder, below) === resolve(path))
|
|
133
|
+
}
|