Implement docs file generation & docs in the luau type definitions file

This commit is contained in:
Filip Tibell 2023-01-26 19:36:06 -05:00
parent ee5b67bf3a
commit 0657e05cc0
No known key found for this signature in database
13 changed files with 1175 additions and 105 deletions

View file

@ -79,6 +79,16 @@ jobs:
asset_name: luneTypes.d.luau
asset_content_type: application/x-luau
- name: Upload documentation file to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: luneDocs.json
asset_name: luneDocs.json
asset_content_type: application/json
release:
needs: ["init", "create-release"]
strategy:

View file

@ -3,6 +3,7 @@
"luau-lsp.sourcemap.enabled": false,
"luau-lsp.types.roblox": false,
"luau-lsp.types.definitionFiles": ["luneTypes.d.luau"],
"luau-lsp.types.documentationFiles": ["luneDocs.json"],
"luau-lsp.require.mode": "relativeToFile",
// Rust
"rust-analyzer.check.command": "clippy",

181
Cargo.lock generated
View file

@ -8,6 +8,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.68"
@ -135,6 +144,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "beef"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -170,6 +185,12 @@ version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "bytecount"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0fdd54b507df8f22012890aadd099979befdba27713c767993f8380112ca7c"
[[package]]
name = "cc"
version = "1.0.78"
@ -228,6 +249,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "crc32fast"
version = "1.3.2"
@ -246,6 +273,19 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn",
]
[[package]]
name = "erased-serde"
version = "0.3.24"
@ -301,6 +341,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
@ -310,6 +356,34 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "full_moon"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d58cb343df2e63e8a496de3e344e5f2b97f010bc1359e994be38a99586888f5"
dependencies = [
"bytecount",
"cfg-if",
"derive_more",
"full_moon_derive",
"logos",
"paste",
"serde",
"smol_str",
]
[[package]]
name = "full_moon_derive"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99b4bd12ce56927d1dc5478d21528ea8c4b93ca85ff8f8043b6a5351a2a3c6f7"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-core"
version = "0.3.25"
@ -368,6 +442,12 @@ dependencies = [
"slab",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.0"
@ -393,6 +473,16 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -460,6 +550,29 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "logos"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-derive"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c"
dependencies = [
"beef",
"fnv",
"proc-macro2",
"quote",
"regex-syntax",
"syn",
]
[[package]]
name = "luau0-src"
version = "0.5.1+luau558"
@ -475,8 +588,10 @@ version = "0.1.3"
dependencies = [
"anyhow",
"clap",
"full_moon",
"mlua",
"os_str_bytes",
"regex",
"serde",
"serde_json",
"smol",
@ -548,6 +663,25 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]]
name = "paste"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880"
dependencies = [
"paste-impl",
"proc-macro-hack",
]
[[package]]
name = "paste-impl"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6"
dependencies = [
"proc-macro-hack",
]
[[package]]
name = "percent-encoding"
version = "2.2.0"
@ -610,6 +744,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.50"
@ -628,6 +768,23 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "ring"
version = "0.16.20"
@ -649,6 +806,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.36.7"
@ -691,6 +857,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "semver"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
[[package]]
name = "serde"
version = "1.0.152"
@ -767,6 +939,15 @@ dependencies = [
"futures-lite",
]
[[package]]
name = "smol_str"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7475118a28b7e3a2e157ce0131ba8c5526ea96e90ee601d9f6bb2e286a35ab44"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
version = "0.4.7"

View file

@ -26,10 +26,12 @@ panic = "abort" # Remove extra panic info
anyhow = "1.0.68"
os_str_bytes = "6.4.1"
regex = "1.7.1"
serde_json = "1.0.91"
smol = "1.3.0"
ureq = "2.6.2"
clap = { version = "4.1.1", features = ["derive"] }
full_moon = { version = "0.17.0", features = ["roblox"] }
mlua = { version = "0.8.7", features = ["luau", "async", "serialize"] }
serde = { version = "1.0.152", features = ["derive"] }

110
README.md
View file

@ -33,102 +33,15 @@ Check out the examples on how to write a script in the [.lune](.lune) folder ! <
A great starting point and walkthrough of Lune can be found in the [Hello, Lune](.lune/hello_lune.luau) example.
<details>
<summary><b>🔎 Full list of APIs</b></summary>
<summary><b>🔎 List of APIs</b></summary>
<details>
<summary><b>console</b> - Logging & formatting</summary>
`console` - Logging & formatting <br />
`fs` - Filesystem <br />
`net` - Networking <br />
`process` - Current process & child processes <br />
`task` - Task scheduler & thread spawning <br />
```lua
type console = {
resetColor: () -> (),
setColor: (color: "black" | "red" | "green" | "yellow" | "blue" | "purple" | "cyan" | "white") -> (),
resetStyle: () -> (),
setStyle: (color: "bold" | "dim") -> (),
format: (...any) -> (string),
log: (...any) -> (),
info: (...any) -> (),
warn: (...any) -> (),
error: (...any) -> (),
}
```
</details>
<details>
<summary><b>fs</b> - Filesystem</summary>
```lua
type fs = {
readFile: (path: string) -> string,
readDir: (path: string) -> { string },
writeFile: (path: string, contents: string) -> (),
writeDir: (path: string) -> (),
removeFile: (path: string) -> (),
removeDir: (path: string) -> (),
isFile: (path: string) -> boolean,
isDir: (path: string) -> boolean,
}
```
</details>
<details>
<summary><b>net</b> - Networking</summary>
```lua
type net = {
request: (config: string | {
url: string,
method: ("GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH")?,
headers: { [string]: string }?,
body: string?,
}) -> {
ok: boolean,
statusCode: number,
statusMessage: string,
headers: { [string]: string },
body: string,
},
jsonEncode: (value: any, pretty: boolean?) -> string,
jsonDecode: (encoded: string) -> any,
}
```
</details>
<details>
<summary><b>process</b> - Current process & child processes</summary>
```lua
type process = {
args: { string },
env: { [string]: string? },
exit: (code: number?) -> (),
spawn: (program: string, params: { string }?) -> {
ok: boolean,
code: number,
stdout: string,
stderr: string,
},
}
```
</details>
<details>
<summary><b>task</b> - Task scheduler & thread spawning</summary>
```lua
type task = {
cancel: (thread: thread) -> (),
defer: <T...>(functionOrThread: thread | (T...) -> (...any), T...) -> thread,
delay: <T...>(duration: number?, functionOrThread: thread | (T...) -> (...any), T...) -> thread,
spawn: <T...>(functionOrThread: thread | (T...) -> (...any), T...) -> thread,
wait: (duration: number?) -> (number),
}
```
</details>
Documentation for individual members and types can be found using your editor of choice and [Luau LSP](https://github.com/JohnnyMorganz/luau-lsp).
</details>
@ -174,9 +87,12 @@ Lune puts developer experience first, and as such provides type definitions and
<details>
<summary>Luau LSP</summary>
1. Use `lune --download-luau-types` to download Luau types (`luneTypes.d.luau`) to the current directory
2. Set your definition files setting to include `luneTypes.d.luau`
3. Set the require mode setting to `relativeToFile`
1. Set the require mode setting to `relativeToFile`
2. Use `lune --download-luau-types` to download Luau types (`luneTypes.d.luau`) to the current directory
3. Set your definition files setting to include `luneTypes.d.luau`
4. Generate the documentation file using `lune --generate-docs-file`
- NOTE: This is a temporary solution and a docs file separate from type definitions will not be necessary in the future
5. Set your documentation files setting to include `luneDocs.json`
An example of these settings can be found in the [.vscode](.vscode) folder in this repository

305
luneDocs.json Normal file
View file

@ -0,0 +1,305 @@
{
"@roblox/global/console": {
"code_sample": "",
"documentation": "Logging & formatting",
"keys": {
"console": "@roblox/global/console.console"
},
"learn_more_link": ""
},
"@roblox/global/console.error": {
"code_sample": "",
"documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stderr.\n\nThis will also prepend an [ERROR] tag at the beginning of the message.\n\nUsing this function will automatically set the exit code of the process\nto 1, unless it gets manually specified afterwards using `process.exit`.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.format": {
"code_sample": "",
"documentation": "Formats arguments into a human-readable string with syntax highlighting for tables.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.info": {
"code_sample": "",
"documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stdout.\n\nThis will also prepend an [INFO] tag at the beginning of the message.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.log": {
"code_sample": "",
"documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stdout.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.resetColor": {
"code_sample": "",
"documentation": "Resets the current persistent output color.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.resetStyle": {
"code_sample": "",
"documentation": "Resets the current persistent output style.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.setColor": {
"code_sample": "",
"documentation": "Sets the current persistent output color.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.setStyle": {
"code_sample": "",
"documentation": "Sets the current persistent output style.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/console.warn": {
"code_sample": "",
"documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stdout.\n\nThis will also prepend an [INFO] tag at the beginning of the message.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs": {
"code_sample": "",
"documentation": "Filesystem",
"keys": {
"fs": "@roblox/global/fs.fs"
},
"learn_more_link": ""
},
"@roblox/global/fs.isDir": {
"code_sample": "",
"documentation": "Checks if a given path is a directory.\n\nAn error will be thrown in the following situations:\n\n* The current process lacks permissions to read at `path`.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs.isFile": {
"code_sample": "",
"documentation": "Checks if a given path is a file.\n\nAn error will be thrown in the following situations:\n\n* The current process lacks permissions to read at `path`.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs.readDir": {
"code_sample": "",
"documentation": "Reads entries in a directory at `path`.\n\nAn error will be thrown in the following situations:\n\n* `path` does not point to an existing directory.\n* The current process lacks permissions to read the contents of the directory.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs.readFile": {
"code_sample": "",
"documentation": "Reads a file at `path`.\n\nAn error will be thrown in the following situations:\n\n* `path` does not point to an existing file.\n* The current process lacks permissions to read the file.\n* The contents of the file cannot be read as a UTF-8 string.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs.removeDir": {
"code_sample": "",
"documentation": "Removes a directory and all of its contents.\n\nAn error will be thrown in the following situations:\n\n* `path` is not an existing and empty directory.\n* The current process lacks permissions to remove the directory.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs.removeFile": {
"code_sample": "",
"documentation": "Removes a file.\n\nAn error will be thrown in the following situations:\n\n* `path` does not point to an existing file.\n* The current process lacks permissions to remove the file.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs.writeDir": {
"code_sample": "",
"documentation": "Creates a directory and its parent directories if they are missing.\n\nAn error will be thrown in the following situations:\n\n* `path` already points to an existing file or directory.\n* The current process lacks permissions to create the directory or its missing parents.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/fs.writeFile": {
"code_sample": "",
"documentation": "Writes to a file at `path`.\n\nAn error will be thrown in the following situations:\n\n* The file's parent directory does not exist.\n* The current process lacks permissions to write to the file.\n* Some other I/O error occurred.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/net": {
"code_sample": "",
"documentation": "Networking",
"keys": {
"net": "@roblox/global/net.net"
},
"learn_more_link": ""
},
"@roblox/global/net.jsonDecode": {
"code_sample": "",
"documentation": "Decodes the given JSON string into a lua value.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/net.jsonEncode": {
"code_sample": "",
"documentation": "Encodes the given value as JSON.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/net.request": {
"code_sample": "",
"documentation": "Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received.\n\nOnly throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/process": {
"code_sample": "",
"documentation": "Current process & child processes",
"keys": {
"process": "@roblox/global/process.process"
},
"learn_more_link": ""
},
"@roblox/global/process.args": {
"code_sample": "",
"documentation": "The arguments given when running the Lune script.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/process.env": {
"code_sample": "",
"documentation": "Current environment variables for this process.\n\nSetting a value on this table will set the corresponding environment variable.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/process.exit": {
"code_sample": "",
"documentation": "Exits the currently running script as soon as possible with the given exit code.\n\nExit code 0 is treated as a successful exit, any other value is treated as an error.\n\nSetting the exit code using this function will override any otherwise automatic exit code.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/process.spawn": {
"code_sample": "",
"documentation": "Spawns a child process that will run the program `program` with the given `params` as arguments, and returns a dictionary that describes the final status and ouput of the child process.",
"learn_more_link": "",
"params": [],
"returns": []
},
"@roblox/global/task": {
"code_sample": "",
"documentation": "Task scheduler & thread spawning",
"keys": {
"task": "@roblox/global/task.task"
},
"learn_more_link": ""
},
"@roblox/global/task.cancel": {
"code_sample": "",
"documentation": "Stops a currently scheduled thread from resuming.",
"learn_more_link": "",
"params": [
{
"documentation": "@roblox/global/task.cancel/param/0",
"name": "thread"
}
],
"returns": []
},
"@roblox/global/task.cancel/param/0": {
"documentation": "The thread to cancel"
},
"@roblox/global/task.defer": {
"code_sample": "",
"documentation": "Defers a thread or function to run at the end of the current task queue.",
"learn_more_link": "",
"params": [
{
"documentation": "@roblox/global/task.defer/param/0",
"name": "functionOrThread"
}
],
"returns": [
"@roblox/global/task.defer/return/0"
]
},
"@roblox/global/task.defer/param/0": {
"documentation": "The function or thread to defer"
},
"@roblox/global/task.defer/return/0": {
"documentation": "The thread that will be deferred"
},
"@roblox/global/task.delay": {
"code_sample": "",
"documentation": "Delays a thread or function to run after `duration` seconds.",
"learn_more_link": "",
"params": [
{
"documentation": "@roblox/global/task.delay/param/0",
"name": "functionOrThread"
}
],
"returns": [
"@roblox/global/task.delay/return/0"
]
},
"@roblox/global/task.delay/param/0": {
"documentation": "The function or thread to delay"
},
"@roblox/global/task.delay/return/0": {
"documentation": "The thread that will be delayed"
},
"@roblox/global/task.spawn": {
"code_sample": "",
"documentation": "Instantly runs a thread or function.\n\nIf the spawned task yields, the thread that spawned the task\nwill resume, letting the spawned task run in the background.",
"learn_more_link": "",
"params": [
{
"documentation": "@roblox/global/task.spawn/param/0",
"name": "functionOrThread"
}
],
"returns": [
"@roblox/global/task.spawn/return/0"
]
},
"@roblox/global/task.spawn/param/0": {
"documentation": "The function or thread to spawn"
},
"@roblox/global/task.spawn/return/0": {
"documentation": "The thread that was spawned"
},
"@roblox/global/task.wait": {
"code_sample": "",
"documentation": "Waits for the given duration, with a minimum wait time of 10 milliseconds.",
"learn_more_link": "",
"params": [
{
"documentation": "@roblox/global/task.wait/param/0",
"name": "duration"
}
],
"returns": [
"@roblox/global/task.wait/return/0"
]
},
"@roblox/global/task.wait/param/0": {
"documentation": "The amount of time to wait"
},
"@roblox/global/task.wait/return/0": {
"documentation": "The exact amount of time waited"
}
}

View file

@ -1,29 +1,192 @@
-- Lune v0.1.3
--[=[
@class console
Logging & formatting
]=]
declare console: {
--[=[
@within console
Resets the current persistent output color.
]=]
resetColor: () -> (),
--[=[
@within console
Sets the current persistent output color.
]=]
setColor: (color: "black" | "red" | "green" | "yellow" | "blue" | "purple" | "cyan" | "white") -> (),
--[=[
@within console
Resets the current persistent output style.
]=]
resetStyle: () -> (),
--[=[
@within console
Sets the current persistent output style.
]=]
setStyle: (style: "bold" | "dim") -> (),
--[=[
@within console
Formats arguments into a human-readable string with syntax highlighting for tables.
]=]
format: (...any) -> (string),
--[=[
@within console
Prints arguments as a human-readable string with syntax highlighting for tables to stdout.
]=]
log: (...any) -> (),
--[=[
@within console
Prints arguments as a human-readable string with syntax highlighting for tables to stdout.
This will also prepend an [INFO] tag at the beginning of the message.
]=]
info: (...any) -> (),
--[=[
@within console
Prints arguments as a human-readable string with syntax highlighting for tables to stdout.
This will also prepend an [INFO] tag at the beginning of the message.
]=]
warn: (...any) -> (),
--[=[
@within console
Prints arguments as a human-readable string with syntax highlighting for tables to stderr.
This will also prepend an [ERROR] tag at the beginning of the message.
Using this function will automatically set the exit code of the process
to 1, unless it gets manually specified afterwards using `process.exit`.
]=]
error: (...any) -> (),
}
--[=[
@class fs
Filesystem
]=]
declare fs: {
--[=[
@within fs
Reads a file at `path`.
An error will be thrown in the following situations:
* `path` does not point to an existing file.
* The current process lacks permissions to read the file.
* The contents of the file cannot be read as a UTF-8 string.
* Some other I/O error occurred.
]=]
readFile: (path: string) -> string,
--[=[
@within fs
Reads entries in a directory at `path`.
An error will be thrown in the following situations:
* `path` does not point to an existing directory.
* The current process lacks permissions to read the contents of the directory.
* Some other I/O error occurred.
]=]
readDir: (path: string) -> { string },
--[=[
@within fs
Writes to a file at `path`.
An error will be thrown in the following situations:
* The file's parent directory does not exist.
* The current process lacks permissions to write to the file.
* Some other I/O error occurred.
]=]
writeFile: (path: string, contents: string) -> (),
--[=[
@within fs
Creates a directory and its parent directories if they are missing.
An error will be thrown in the following situations:
* `path` already points to an existing file or directory.
* The current process lacks permissions to create the directory or its missing parents.
* Some other I/O error occurred.
]=]
writeDir: (path: string) -> (),
--[=[
@within fs
Removes a file.
An error will be thrown in the following situations:
* `path` does not point to an existing file.
* The current process lacks permissions to remove the file.
* Some other I/O error occurred.
]=]
removeFile: (path: string) -> (),
--[=[
@within fs
Removes a directory and all of its contents.
An error will be thrown in the following situations:
* `path` is not an existing and empty directory.
* The current process lacks permissions to remove the directory.
* Some other I/O error occurred.
]=]
removeDir: (path: string) -> (),
--[=[
@within fs
Checks if a given path is a file.
An error will be thrown in the following situations:
* The current process lacks permissions to read at `path`.
* Some other I/O error occurred.
]=]
isFile: (path: string) -> boolean,
--[=[
@within fs
Checks if a given path is a directory.
An error will be thrown in the following situations:
* The current process lacks permissions to read at `path`.
* Some other I/O error occurred.
]=]
isDir: (path: string) -> boolean,
}
--[=[
@class net
Networking
]=]
declare net: {
--[=[
@within net
Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received.
Only throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes.
]=]
request: (config: string | {
url: string,
method: ("GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH")?,
@ -36,14 +199,55 @@ declare net: {
headers: { [string]: string },
body: string,
},
--[=[
@within net
Encodes the given value as JSON.
]=]
jsonEncode: (value: any, pretty: boolean?) -> string,
--[=[
@within net
Decodes the given JSON string into a lua value.
]=]
jsonDecode: (encoded: string) -> any,
}
--[=[
@class process
Current process & child processes
]=]
declare process: {
--[=[
@within process
The arguments given when running the Lune script.
]=]
args: { string },
--[=[
@within process
Current environment variables for this process.
Setting a value on this table will set the corresponding environment variable.
]=]
env: { [string]: string? },
--[=[
@within process
Exits the currently running script as soon as possible with the given exit code.
Exit code 0 is treated as a successful exit, any other value is treated as an error.
Setting the exit code using this function will override any otherwise automatic exit code.
]=]
exit: (code: number?) -> (),
--[=[
@within process
Spawns a child process that will run the program `program` with the given `params` as arguments, and returns a dictionary that describes the final status and ouput of the child process.
]=]
spawn: (program: string, params: { string }?) -> {
ok: boolean,
code: number,
@ -52,10 +256,57 @@ declare process: {
},
}
--[=[
@class task
Task scheduler & thread spawning
]=]
declare task: {
--[=[
@within task
Stops a currently scheduled thread from resuming.
@param thread The thread to cancel
]=]
cancel: (thread: thread) -> (),
--[=[
@within task
Defers a thread or function to run at the end of the current task queue.
@param functionOrThread The function or thread to defer
@return The thread that will be deferred
]=]
defer: <T...>(functionOrThread: thread | (T...) -> (...any), T...) -> thread,
--[=[
@within task
Delays a thread or function to run after `duration` seconds.
@param functionOrThread The function or thread to delay
@return The thread that will be delayed
]=]
delay: <T...>(duration: number?, functionOrThread: thread | (T...) -> (...any), T...) -> thread,
--[=[
@within task
Instantly runs a thread or function.
If the spawned task yields, the thread that spawned the task
will resume, letting the spawned task run in the background.
@param functionOrThread The function or thread to spawn
@return The thread that was spawned
]=]
spawn: <T...>(functionOrThread: thread | (T...) -> (...any), T...) -> thread,
--[=[
@within task
Waits for the given duration, with a minimum wait time of 10 milliseconds.
@param duration The amount of time to wait
@return The exact amount of time waited
]=]
wait: (duration: number?) -> (number),
}

View file

@ -1,22 +1,28 @@
use std::{fs::read_to_string, process::ExitCode};
use std::process::ExitCode;
use anyhow::Result;
use clap::{CommandFactory, Parser};
use lune::Lune;
use smol::fs::{read_to_string, write};
use crate::utils::{
use crate::{
gen::generate_docs_json_from_definitions,
utils::{
files::find_parse_file_path,
github::Client as GithubClient,
listing::{find_lune_scripts, print_lune_scripts, sort_lune_scripts},
},
};
const LUNE_SELENE_FILE_NAME: &str = "lune.yml";
const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau";
const LUNE_DOCS_FILE_NAME: &str = "luneDocs.json";
/// Lune CLI
#[derive(Parser, Debug, Default)]
#[command(author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
/// Path to the file to run, or the name
/// of a luau file in a lune directory
@ -37,6 +43,10 @@ pub struct Cli {
/// definitions file to the current directory
#[clap(long)]
download_luau_types: bool,
/// Pass this flag to generate the Lune documentation file
/// from a luau type definitions file in the current directory
#[clap(long)]
generate_docs_file: bool,
}
#[allow(dead_code)]
@ -123,10 +133,18 @@ impl Cli {
.await?;
}
}
// Generate docs file, if wanted
if self.generate_docs_file {
let defs_contents = read_to_string(LUNE_LUAU_FILE_NAME).await?;
let docs_root = generate_docs_json_from_definitions(&defs_contents, "roblox/global")?;
let docs_contents = serde_json::to_string_pretty(&docs_root)?;
write(LUNE_DOCS_FILE_NAME, &docs_contents).await?;
}
if self.script_path.is_none() {
// Only downloading types without running a script is completely
// fine, and we should just exit the program normally afterwards
if download_types_requested {
// Same thing goes for generating the docs file
if download_types_requested || self.generate_docs_file {
return Ok(ExitCode::SUCCESS);
}
// HACK: We know that we didn't get any arguments here but since
@ -138,7 +156,7 @@ impl Cli {
}
// Parse and read the wanted file
let file_path = find_parse_file_path(&self.script_path.unwrap())?;
let file_contents = read_to_string(&file_path)?;
let file_contents = read_to_string(&file_path).await?;
// Display the file path relative to cwd with no extensions in stack traces
let file_display_name = file_path.with_extension("").display().to_string();
// Create a new lune object with all globals & run the script

46
src/cli/gen/doc.rs Normal file
View file

@ -0,0 +1,46 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DocsGlobal {
pub documentation: String,
pub keys: HashMap<String, String>,
pub learn_more_link: String,
pub code_sample: String,
}
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DocsFunctionParamLink {
pub name: String,
pub documentation: String,
}
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DocsFunction {
#[serde(skip)]
pub global_name: String,
pub documentation: String,
pub params: Vec<DocsFunctionParamLink>,
pub returns: Vec<String>,
pub learn_more_link: String,
pub code_sample: String,
}
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DocsParam {
#[serde(skip)]
pub global_name: String,
#[serde(skip)]
pub function_name: String,
pub documentation: String,
}
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DocsReturn {
#[serde(skip)]
pub global_name: String,
#[serde(skip)]
pub function_name: String,
pub documentation: String,
}

83
src/cli/gen/mod.rs Normal file
View file

@ -0,0 +1,83 @@
use std::collections::HashMap;
use anyhow::Result;
use regex::Regex;
use serde_json::{Map, Value};
use full_moon::{parse as parse_luau_ast, visitors::Visitor};
mod doc;
mod tag;
mod visitor;
use self::{doc::DocsFunctionParamLink, visitor::DocumentationVisitor};
fn parse_definitions(contents: &str) -> Result<DocumentationVisitor> {
let (regex, replacement) = (
Regex::new(r#"declare (?P<n>\w+): \{"#).unwrap(),
r#"export type $n = {"#,
);
let defs_ast = parse_luau_ast(&regex.replace_all(contents, replacement))?;
let mut visitor = DocumentationVisitor::new();
visitor.visit_ast(&defs_ast);
Ok(visitor)
}
pub fn generate_docs_json_from_definitions(contents: &str, namespace: &str) -> Result<Value> {
let visitor = parse_definitions(contents)?;
/*
Extract globals, functions, params, returns from the visitor
Here we will also convert the plain names into proper namespaced names according to the spec at
https://raw.githubusercontent.com/MaximumADHD/Roblox-Client-Tracker/roblox/api-docs/en-us.json
*/
let mut map = Map::new();
for (name, mut doc) in visitor.globals {
doc.keys = doc
.keys
.iter()
.map(|(key, value)| (key.clone(), format!("@{namespace}/{name}.{value}")))
.collect::<HashMap<String, String>>();
map.insert(format!("@{namespace}/{name}"), serde_json::to_value(doc)?);
}
for (name, mut doc) in visitor.functions {
doc.params = doc
.params
.iter()
.map(|param| DocsFunctionParamLink {
name: param.name.clone(),
documentation: format!(
"@{namespace}/{}.{name}/param/{}",
doc.global_name, param.documentation
),
})
.collect::<Vec<_>>();
doc.returns = doc
.returns
.iter()
.map(|ret| format!("@{namespace}/{}.{name}/return/{ret}", doc.global_name))
.collect::<Vec<_>>();
map.insert(
format!("@{namespace}/{}.{name}", doc.global_name),
serde_json::to_value(doc)?,
);
}
for (name, doc) in visitor.params {
map.insert(
format!(
"@{namespace}/{}.{}/param/{name}",
doc.global_name, doc.function_name
),
serde_json::to_value(doc)?,
);
}
for (name, doc) in visitor.returns {
map.insert(
format!(
"@{namespace}/{}.{}/return/{name}",
doc.global_name, doc.function_name
),
serde_json::to_value(doc)?,
);
}
Ok(Value::Object(map))
}

64
src/cli/gen/tag.rs Normal file
View file

@ -0,0 +1,64 @@
use anyhow::{bail, Result};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DocsTagKind {
Class,
Within,
Param,
Return,
}
impl DocsTagKind {
pub fn parse(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_ref() {
"class" => Ok(Self::Class),
"within" => Ok(Self::Within),
"param" => Ok(Self::Param),
"return" => Ok(Self::Return),
s => bail!("Unknown docs tag: '{}'", s),
}
}
}
#[derive(Clone, Debug)]
pub struct DocsTag {
pub kind: DocsTagKind,
pub name: String,
pub contents: String,
}
#[derive(Clone, Debug)]
pub struct DocsTagList {
tags: Vec<DocsTag>,
}
impl DocsTagList {
pub fn new() -> Self {
Self { tags: vec![] }
}
pub fn push(&mut self, tag: DocsTag) {
self.tags.push(tag);
}
pub fn contains(&mut self, kind: DocsTagKind) -> bool {
self.tags.iter().any(|tag| tag.kind == kind)
}
pub fn find(&mut self, kind: DocsTagKind) -> Option<&DocsTag> {
self.tags.iter().find(|tag| tag.kind == kind)
}
pub fn is_empty(&self) -> bool {
self.tags.is_empty()
}
}
impl IntoIterator for DocsTagList {
type Item = DocsTag;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.tags.into_iter()
}
}

188
src/cli/gen/visitor.rs Normal file
View file

@ -0,0 +1,188 @@
use full_moon::{
ast::types::{ExportedTypeDeclaration, TypeField, TypeFieldKey},
tokenizer::{Token, TokenType},
visitors::Visitor,
};
use regex::Regex;
use super::{
doc::{DocsFunction, DocsFunctionParamLink, DocsGlobal, DocsParam, DocsReturn},
tag::{DocsTag, DocsTagKind, DocsTagList},
};
pub struct DocumentationVisitor {
pub globals: Vec<(String, DocsGlobal)>,
pub functions: Vec<(String, DocsFunction)>,
pub params: Vec<(String, DocsParam)>,
pub returns: Vec<(String, DocsReturn)>,
tag_regex: Regex,
}
impl DocumentationVisitor {
pub fn new() -> Self {
let tag_regex = Regex::new(r#"^@(\w+)\s+(\w+)(.*)$"#).unwrap();
Self {
globals: vec![],
functions: vec![],
params: vec![],
returns: vec![],
tag_regex,
}
}
pub fn parse_moonwave_style_tag(&self, line: &str) -> Option<DocsTag> {
if self.tag_regex.is_match(line) {
let captures = self.tag_regex.captures(line).unwrap();
let tag_kind = captures.get(1).unwrap().as_str();
let tag_name = captures.get(2).unwrap().as_str();
let tag_contents = captures.get(3).unwrap().as_str();
Some(DocsTag {
kind: DocsTagKind::parse(tag_kind).unwrap(),
name: tag_name.to_string(),
contents: tag_contents.to_string(),
})
} else {
None
}
}
pub fn parse_moonwave_style_comment(&self, comment: &str) -> (String, DocsTagList) {
let lines = comment.lines().map(str::trim).collect::<Vec<_>>();
let indent_len = lines.iter().fold(usize::MAX, |acc, line| {
let first = line.chars().enumerate().find_map(|(idx, ch)| {
if ch.is_alphanumeric() {
Some(idx)
} else {
None
}
});
if let Some(first_alphanumeric) = first {
if first_alphanumeric > 0 {
acc.min(first_alphanumeric - 1)
} else {
0
}
} else {
acc
}
});
let unindented_lines = lines.iter().map(|line| &line[indent_len..]);
let mut doc_lines = Vec::new();
let mut doc_tags = DocsTagList::new();
for line in unindented_lines {
if let Some(tag) = self.parse_moonwave_style_tag(line) {
doc_tags.push(tag);
} else {
doc_lines.push(line);
}
}
(doc_lines.join("\n").trim().to_owned(), doc_tags)
}
fn extract_moonwave_comment(&mut self, token: &Token) -> Option<(String, DocsTagList)> {
if let TokenType::MultiLineComment { comment, .. } = token.token_type() {
let (doc, tags) = self.parse_moonwave_style_comment(comment);
if doc.is_empty() && tags.is_empty() {
None
} else {
Some((doc, tags))
}
} else {
None
}
}
}
impl Visitor for DocumentationVisitor {
fn visit_exported_type_declaration(&mut self, node: &ExportedTypeDeclaration) {
for token in node.export_token().leading_trivia() {
if let Some((doc, mut tags)) = self.extract_moonwave_comment(token) {
if tags.contains(DocsTagKind::Class) {
self.globals.push((
node.type_declaration().type_name().token().to_string(),
DocsGlobal {
documentation: doc,
..Default::default()
},
));
break;
}
}
}
}
fn visit_type_field(&mut self, node: &TypeField) {
// Parse out names, moonwave comments from the ast
let mut parsed_data = Vec::new();
if let TypeFieldKey::Name(name) = node.key() {
for token in name.leading_trivia() {
if let Some((doc, mut tags)) = self.extract_moonwave_comment(token) {
if let Some(within) = tags.find(DocsTagKind::Within).map(ToOwned::to_owned) {
parsed_data.push((within.name, name, doc, tags));
}
}
}
}
for (global_name, name, doc, tags) in parsed_data {
// Find the global definition, which is guaranteed to
// be visited and parsed before its inner members, and
// add a ref to the found function / member to it
let name = name.token().to_string();
for (name, global) in &mut self.globals {
if name == &global_name {
global.keys.insert(name.clone(), name.clone());
}
}
// Look through tags to find and create doc params and returns
let mut param_links = Vec::new();
let mut return_links = Vec::new();
for tag in tags {
match tag.kind {
DocsTagKind::Param => {
let idx_string = param_links.len().to_string();
self.params.push((
idx_string.clone(),
DocsParam {
global_name: global_name.clone(),
function_name: name.clone(),
documentation: tag.contents.trim().to_owned(),
},
));
param_links.push(DocsFunctionParamLink {
name: tag.name.clone(),
documentation: idx_string.clone(),
});
}
DocsTagKind::Return => {
// NOTE: Returns don't have names but we still parse
// them as such, so we should concat name & contents
let doc = format!("{} {}", tag.name.trim(), tag.contents.trim());
let idx_string = return_links.len().to_string();
self.returns.push((
idx_string.clone(),
DocsReturn {
global_name: global_name.clone(),
function_name: name.clone(),
documentation: doc,
},
));
return_links.push(idx_string.clone());
}
_ => {}
}
}
// Finally, add our complete doc
// function with links into the list
self.functions.push((
name,
DocsFunction {
global_name,
documentation: doc,
params: param_links,
returns: return_links,
..Default::default()
},
));
}
}
}

View file

@ -1,6 +1,10 @@
#![deny(clippy::all)]
#![warn(clippy::cargo, clippy::pedantic)]
#![allow(clippy::needless_pass_by_value, clippy::match_bool)]
#![allow(
clippy::needless_pass_by_value,
clippy::match_bool,
clippy::module_name_repetitions
)]
use std::process::ExitCode;
@ -8,6 +12,7 @@ use anyhow::Result;
use clap::Parser;
mod cli;
mod gen;
mod utils;
use cli::Cli;