mirror of
https://github.com/Ukendio/jecs.git
synced 2026-02-04 15:15:21 +00:00
Automatically build a rbxm of documentation for Studio users (#287)
* Add types for (Cached)Query.has (#286) * ts type update * changed to hard tabs * reverted back to old formatting for some * more fixes * only keep query has * Build studio docs * Fix the nightly.link * Make the nightly.link work for the main Jecs repo, rename the workflow * Add alias resolution * Artifacts expire, run the action every month * Add deleted flag to removed handler typings (#288) * update types * change "StatefulHook" to "HookWithData" and "StatelessHook" to "HookWithDeleted" * Update deleted flag type --------- Co-authored-by: maeriil <104389763+maeriil@users.noreply.github.com> Co-authored-by: m10 <165406716+mrkboy10@users.noreply.github.com> Co-authored-by: Marcus <ukendio@gmail.com>
This commit is contained in:
parent
64f8750f3c
commit
e494c35b78
8 changed files with 382 additions and 1 deletions
26
.github/workflows/build-studio-docs.yaml
vendored
Normal file
26
.github/workflows/build-studio-docs.yaml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: build-studio-docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
schedule:
|
||||||
|
- cron: "10 0 1 * *" # Artifacts expire every 90 days. Doesn't hurt to just run this workflow first day of every month at 00:10 UTC.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
studio-docs:
|
||||||
|
name: Build Studio Docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
|
||||||
|
- name: Build Studio Docs
|
||||||
|
run: zune run scripts/build_studio_docs
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: studio_docs
|
||||||
|
path: studio_docs.rbxm
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -75,3 +75,4 @@ genhtml.perl
|
||||||
rokit.toml
|
rokit.toml
|
||||||
package-lock.json
|
package-lock.json
|
||||||
mirror.luau
|
mirror.luau
|
||||||
|
studio_docs/
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ This repository includes a few subfolders that can help you get started with jec
|
||||||
- examples:
|
- examples:
|
||||||
These are larger programs that showcase real use cases and can help you understand how everything fits together.
|
These are larger programs that showcase real use cases and can help you understand how everything fits together.
|
||||||
|
|
||||||
|
If you wish to view the documentation inside Roblox Studio, you can find an importable model file [here](https://nightly.link/Ukendio/jecs/workflows/build-studio-docs.yaml/main/studio_docs.zip).
|
||||||
|
|
||||||
### Benchmarks
|
### Benchmarks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ local ECS_ID = jecs.ECS_ID
|
||||||
local __ = jecs.Wildcard
|
local __ = jecs.Wildcard
|
||||||
local pair = jecs.pair
|
local pair = jecs.pair
|
||||||
|
|
||||||
local prettify = require("@tools/entity_visualiser").prettify
|
local prettify = require("@modules/entity_visualiser").prettify
|
||||||
|
|
||||||
local pe = prettify
|
local pe = prettify
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@
|
||||||
wally = "upliftgames/wally@0.3.2"
|
wally = "upliftgames/wally@0.3.2"
|
||||||
rojo = "rojo-rbx/rojo@7.7.0-rc.1"
|
rojo = "rojo-rbx/rojo@7.7.0-rc.1"
|
||||||
luau = "luau-lang/luau@0.703.0"
|
luau = "luau-lang/luau@0.703.0"
|
||||||
|
zune = "scythe-technology/zune@0.5.1"
|
||||||
256
scripts/alias_resolver.luau
Normal file
256
scripts/alias_resolver.luau
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
local fs = zune.fs
|
||||||
|
local stdpath = fs.path
|
||||||
|
|
||||||
|
export type FsFileNode = {
|
||||||
|
name: string,
|
||||||
|
kind: "file",
|
||||||
|
parent: FsDirNode?,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FsDirNode = {
|
||||||
|
name: string,
|
||||||
|
kind: "dir",
|
||||||
|
parent: FsDirNode?,
|
||||||
|
children: { [string]: FsNode },
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FsNode = FsFileNode | FsDirNode
|
||||||
|
|
||||||
|
local function build_fs_tree(path: string, parent: FsDirNode?): FsDirNode
|
||||||
|
local children = {}
|
||||||
|
local node = {
|
||||||
|
name = stdpath.basename(path),
|
||||||
|
kind = "dir",
|
||||||
|
parent = parent,
|
||||||
|
children = children,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry in fs.entries(path) do
|
||||||
|
local child_path = stdpath.join(path, entry.name)
|
||||||
|
local child_node: FsNode = if entry.kind == "directory"
|
||||||
|
then build_fs_tree(child_path, node)
|
||||||
|
else { name = entry.name, kind = "file", parent = node }
|
||||||
|
children[entry.name] = child_node
|
||||||
|
end
|
||||||
|
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
local function path_to_node(root: FsDirNode, path: string, traverse_modules: boolean?): FsNode?
|
||||||
|
local names = string.split(path, "/")
|
||||||
|
assert(#names > 0, `Invalid path: {path}`)
|
||||||
|
|
||||||
|
local node = root
|
||||||
|
for idx, name in names do
|
||||||
|
if name == "" then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
if name == "." then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if name == ".." then
|
||||||
|
local parent = node.parent
|
||||||
|
if not parent then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
node = node.parent
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local child = node.children[name]
|
||||||
|
|
||||||
|
if not child and traverse_modules then
|
||||||
|
child = node.children[name .. ".luau"]
|
||||||
|
if not child then
|
||||||
|
child = node.children["init.luau"]
|
||||||
|
if not child then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if child.kind == "file" and idx < #names then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
node = child
|
||||||
|
end
|
||||||
|
|
||||||
|
if traverse_modules and node.kind == "dir" then
|
||||||
|
local possible_module = node.children["init.luau"]
|
||||||
|
if possible_module then
|
||||||
|
return possible_module
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_closest_common_ancestor(from: FsNode, to: FsNode): FsNode?
|
||||||
|
local from_ancestors: { FsDirNode } = {}
|
||||||
|
do
|
||||||
|
local ancestor = from.parent
|
||||||
|
while ancestor ~= nil do
|
||||||
|
table.insert(from_ancestors, ancestor)
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
end
|
||||||
|
|
||||||
|
if #from_ancestors <= 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local to_ancestor_lookup: { [FsDirNode]: true } = {}
|
||||||
|
local to_ancestors: { FsDirNode } = {}
|
||||||
|
do
|
||||||
|
local ancestor = to.parent
|
||||||
|
while ancestor ~= nil do
|
||||||
|
to_ancestor_lookup[ancestor] = true
|
||||||
|
table.insert(to_ancestors, ancestor)
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
end
|
||||||
|
|
||||||
|
if #to_ancestors <= 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local closest_common_ancestor: FsDirNode? = nil
|
||||||
|
for _, ancestor in from_ancestors do
|
||||||
|
if to_ancestor_lookup[ancestor] then
|
||||||
|
closest_common_ancestor = ancestor
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return closest_common_ancestor
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_relative_module_path(from: FsFileNode, to: FsFileNode): string?
|
||||||
|
local common_ancestor = get_closest_common_ancestor(from, to)
|
||||||
|
if not common_ancestor then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local path = ""
|
||||||
|
if not from.parent then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local traverse_from: FsNode = nil :: any
|
||||||
|
local is_init = stdpath.stem(from.name) == "init"
|
||||||
|
if common_ancestor == from.parent then
|
||||||
|
path = if is_init then "@self" else "."
|
||||||
|
traverse_from = from.parent :: FsNode
|
||||||
|
else
|
||||||
|
if not from.parent.parent then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if common_ancestor == from.parent.parent then
|
||||||
|
path = if is_init then "." else ".."
|
||||||
|
traverse_from = from.parent.parent :: FsNode
|
||||||
|
else
|
||||||
|
if not from.parent.parent or not from.parent.parent.parent then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
path = if is_init then ".." else "../.."
|
||||||
|
traverse_from = from.parent.parent.parent :: FsNode
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local from_ancestor = traverse_from.parent :: FsNode?
|
||||||
|
while from_ancestor ~= nil and from_ancestor ~= common_ancestor and from_ancestor ~= common_ancestor.parent do
|
||||||
|
path ..= "/.."
|
||||||
|
from_ancestor = from_ancestor.parent
|
||||||
|
end
|
||||||
|
|
||||||
|
local to_ancestor = to.parent
|
||||||
|
local to_stem = stdpath.stem(to.name)
|
||||||
|
if to_stem == "init" then
|
||||||
|
to_ancestor = assert(to_ancestor).parent
|
||||||
|
end
|
||||||
|
|
||||||
|
local to_ancestors: { FsNode } = {}
|
||||||
|
while to_ancestor ~= nil and to_ancestor ~= common_ancestor do
|
||||||
|
table.insert(to_ancestors, to_ancestor)
|
||||||
|
to_ancestor = to_ancestor.parent
|
||||||
|
end
|
||||||
|
for idx = #to_ancestors, 1, -1 do
|
||||||
|
local descendant = to_ancestors[idx]
|
||||||
|
path ..= `/{descendant.name}`
|
||||||
|
end
|
||||||
|
|
||||||
|
local is_to_init = stdpath.stem(to.name) == "init"
|
||||||
|
path ..= if is_to_init then `/{assert(to.parent).name}` else `/{to_stem}`
|
||||||
|
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_resolver(
|
||||||
|
fs_root: FsDirNode,
|
||||||
|
aliases: { [string]: string }
|
||||||
|
): (path: string, from: string) -> string?
|
||||||
|
local alias_nodes = {}
|
||||||
|
for alias, path in aliases do
|
||||||
|
local node = path_to_node(fs_root, path)
|
||||||
|
if not node then
|
||||||
|
error(`Alias {alias} with path {path} cannot be resolved into a node`)
|
||||||
|
end
|
||||||
|
alias_nodes[alias] = node
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(require_path: string, from: string): string?
|
||||||
|
local alias = string.match(require_path, "^@([^/]+)")
|
||||||
|
if not alias or alias == "self" or alias == "game" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if not aliases[alias] then
|
||||||
|
error(`Unresolvable alias: {alias}`)
|
||||||
|
end
|
||||||
|
local alias_node = alias_nodes[alias]
|
||||||
|
|
||||||
|
local origin_node = path_to_node(fs_root, from, true)
|
||||||
|
if not origin_node or origin_node.kind ~= "file" then
|
||||||
|
error(`The path {from} cannot be resolved into a node`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local target_node: FsNode = alias_node
|
||||||
|
local first_slash_idx = string.find(require_path, "/")
|
||||||
|
if first_slash_idx then
|
||||||
|
local len = #require_path
|
||||||
|
local target_path = string.sub(require_path, first_slash_idx + 1, len)
|
||||||
|
if #target_path + 1 ~= len then
|
||||||
|
assert(alias_node.kind == "dir")
|
||||||
|
target_node = assert(
|
||||||
|
path_to_node(alias_node, target_path, true),
|
||||||
|
`The path {require_path} cannot be resolved into a node`
|
||||||
|
)
|
||||||
|
elseif alias_node.kind == "dir" then
|
||||||
|
target_node = assert(
|
||||||
|
path_to_node(alias_node, "init.luau", true),
|
||||||
|
`The path {require_path} cannot be resolved into a node`
|
||||||
|
)
|
||||||
|
end
|
||||||
|
elseif alias_node.kind == "dir" then
|
||||||
|
target_node = assert(
|
||||||
|
path_to_node(alias_node, "init.luau", true),
|
||||||
|
`The path {require_path} cannot be resolved into a node`
|
||||||
|
)
|
||||||
|
end
|
||||||
|
assert(target_node.kind == "file")
|
||||||
|
|
||||||
|
local path = get_relative_module_path(origin_node, target_node)
|
||||||
|
assert(path, `Failed to navigate from {from} to {require_path}`)
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
build_fs_tree = build_fs_tree,
|
||||||
|
build_resolver = build_resolver,
|
||||||
|
}
|
||||||
90
scripts/build_studio_docs.luau
Normal file
90
scripts/build_studio_docs.luau
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
--!strict
|
||||||
|
-- Copies all docs related files into a dist folder with all aliases resolved to relative require paths
|
||||||
|
local fs = zune.fs
|
||||||
|
local stdpath = fs.path
|
||||||
|
local process = zune.process
|
||||||
|
local alias_resolver = require("./alias_resolver")
|
||||||
|
|
||||||
|
-- stylua: ignore
|
||||||
|
local DISABLED_SCRIPT = [[
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"Disabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
|
||||||
|
local text_files = { "README.md", "LICENSE" }
|
||||||
|
local allowed_extensions = { ".luau", ".json" }
|
||||||
|
local directories = { "examples", "how_to", "modules" }
|
||||||
|
local output = "studio_docs"
|
||||||
|
|
||||||
|
local function traverse(
|
||||||
|
dir: string,
|
||||||
|
fn: (name: string, path: string, local_path: string, metadata: Metadata) -> (),
|
||||||
|
start_name: string?
|
||||||
|
)
|
||||||
|
local local_name = start_name or stdpath.basename(dir)
|
||||||
|
|
||||||
|
for _, entry in fs.entries(dir) do
|
||||||
|
local path = stdpath.join(dir, entry.name)
|
||||||
|
local child = fs.metadata(path)
|
||||||
|
|
||||||
|
local local_path = stdpath.join(local_name, entry.name)
|
||||||
|
fn(entry.name, path, local_path, child)
|
||||||
|
if child.kind == "directory" then
|
||||||
|
traverse(path, fn, local_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if fs.stat(output).kind ~= "none" then
|
||||||
|
fs.deleteDir(output, true)
|
||||||
|
end
|
||||||
|
fs.makeDir(output)
|
||||||
|
fs.copy("src/jecs.luau", `{output}/jecs.luau`)
|
||||||
|
|
||||||
|
for _, file in text_files do
|
||||||
|
local source = fs.readFile(file)
|
||||||
|
local luau_source = `--[\[\n{source}\n]\]`
|
||||||
|
local stem = fs.path.stem(file)
|
||||||
|
fs.writeFile(`{output}/{stem}.server.luau`, luau_source)
|
||||||
|
fs.writeFile(`{output}/{stem}.meta.json`, DISABLED_SCRIPT)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, dir in directories do
|
||||||
|
fs.copy(dir, `{output}/{dir}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local vfs_root = alias_resolver.build_fs_tree(output)
|
||||||
|
local resolve_alias = alias_resolver.build_resolver(vfs_root, { jecs = "jecs.luau", modules = "modules" })
|
||||||
|
traverse(output, function(name, path, local_path, metadata)
|
||||||
|
local extension = stdpath.extension(path)
|
||||||
|
if name == "jecs.luau" or metadata.kind ~= "file" or not table.find(allowed_extensions, extension) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local_path = string.gsub(local_path, "\\", "/") -- vindovs
|
||||||
|
|
||||||
|
local contents = fs.readFile(path)
|
||||||
|
local lines = string.split(contents, "\n")
|
||||||
|
for number, line in lines do
|
||||||
|
local require_path = string.match(line, 'require%("(@[^"]+)"%)')
|
||||||
|
if not require_path then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local resolved_path = resolve_alias(require_path, local_path)
|
||||||
|
if not resolved_path then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
lines[number] = string.gsub(line, require_path, resolved_path)
|
||||||
|
end
|
||||||
|
fs.writeFile(path, table.concat(lines, "\n"))
|
||||||
|
end, "")
|
||||||
|
|
||||||
|
process.run(
|
||||||
|
"rojo",
|
||||||
|
{ "build", "studio_docs.project.json", "-o", "studio_docs.rbxm" },
|
||||||
|
-- Luau
|
||||||
|
{ stdout = "inherit", stderr = "inherit" } :: any
|
||||||
|
)
|
||||||
6
studio_docs.project.json
Normal file
6
studio_docs.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "jecs_studio_docs",
|
||||||
|
"tree": {
|
||||||
|
"$path": "studio_docs"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue