├── selene.toml ├── src ├── api │ ├── index.ts │ └── compatibility.ts ├── utils │ ├── file-utils │ │ ├── types.ts │ │ ├── index.ts │ │ ├── path-utils.ts │ │ └── make-utils.ts │ ├── fetch-github-release │ │ ├── index.ts │ │ ├── identify.ts │ │ ├── downloadAsset.ts │ │ ├── getReleases.ts │ │ ├── types.ts │ │ └── downloadRelease.ts │ ├── http.ts │ ├── replace.ts │ ├── extract.ts │ └── JsonStore.ts ├── modules │ ├── services │ │ ├── init.lua │ │ ├── README.md │ │ ├── package.json │ │ └── index.d.ts │ ├── object-utils │ │ ├── README.md │ │ ├── package.json │ │ ├── init.lua │ │ └── index.d.ts │ ├── zzlib │ │ ├── index.d.ts │ │ └── init.lua │ └── make │ │ ├── README.md │ │ ├── init.lua │ │ ├── package.json │ │ └── index.d.ts ├── core │ ├── index.ts │ ├── build │ │ ├── txt.ts │ │ ├── dir.ts │ │ ├── json.ts │ │ ├── rbx-model.ts │ │ ├── EncodedValue │ │ │ ├── index.d.ts │ │ │ └── init.lua │ │ ├── metadata.ts │ │ ├── json-model.ts │ │ ├── lua.ts │ │ ├── index.ts │ │ └── csv.ts │ ├── Store.ts │ ├── types.ts │ ├── Session.ts │ └── VirtualScript.ts ├── bootstrap.ts ├── index.ts └── Package.ts ├── typings ├── index.d.ts └── Engine.d.ts ├── .gitignore ├── docs ├── requirements.txt ├── index.md ├── assets │ ├── images │ │ ├── site-banner.png │ │ ├── github-asset.png │ │ ├── disabled-script.png │ │ ├── midi-player-src.png │ │ ├── github-tag-version.png │ │ ├── midi-player-screenshot.png │ │ ├── vs-code-logo.svg │ │ └── roact-panel.svg │ ├── overrides │ │ ├── main.html │ │ └── home.html │ └── stylesheets │ │ ├── api-tags.css │ │ └── extra.css ├── api-reference │ ├── overview.md │ ├── rostruct │ │ ├── open.md │ │ ├── clearcache.md │ │ ├── fetch.md │ │ └── fetchlatest.md │ ├── globals.md │ ├── package │ │ ├── require.md │ │ ├── start.md │ │ ├── properties.md │ │ └── build.md │ ├── types.md │ └── file-conversion.md ├── featured │ └── community.md └── getting-started │ ├── installation.md │ ├── execution-model.md │ ├── using-other-projects.md │ ├── creating-your-project.md │ ├── overview.md │ └── publishing-your-project.md ├── img ├── example-vscode-and-roblox.png └── Rostruct.svg ├── .vscode ├── extensions.json └── settings.json ├── .gitattributes ├── .github └── workflows │ ├── eslint.yml │ ├── deploy-docs.yml │ └── release.yml ├── tsconfig.json ├── LICENSE ├── package.json ├── bin ├── bundle-test.sh ├── bundle-prod.sh ├── bundle-test-min.sh ├── test.lua └── runtime.lua ├── .eslintrc.json ├── mkdocs.yml └── README.md /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" 2 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./compatibility"; 2 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /out 3 | /site 4 | /include 5 | /Rostruct.lua 6 | *.tsbuildinfo 7 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | pymdown-extensions 4 | mkdocs-macros-plugin 5 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | - toc 5 | template: home.html 6 | title: Rostruct 7 | --- 8 | -------------------------------------------------------------------------------- /docs/assets/images/site-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richie0866/Rostruct/HEAD/docs/assets/images/site-banner.png -------------------------------------------------------------------------------- /img/example-vscode-and-roblox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richie0866/Rostruct/HEAD/img/example-vscode-and-roblox.png -------------------------------------------------------------------------------- /docs/assets/images/github-asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richie0866/Rostruct/HEAD/docs/assets/images/github-asset.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "roblox-ts.vscode-roblox-ts", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } -------------------------------------------------------------------------------- /docs/assets/images/disabled-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richie0866/Rostruct/HEAD/docs/assets/images/disabled-script.png -------------------------------------------------------------------------------- /docs/assets/images/midi-player-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richie0866/Rostruct/HEAD/docs/assets/images/midi-player-src.png -------------------------------------------------------------------------------- /docs/assets/images/github-tag-version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richie0866/Rostruct/HEAD/docs/assets/images/github-tag-version.png -------------------------------------------------------------------------------- /docs/assets/images/midi-player-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richie0866/Rostruct/HEAD/docs/assets/images/midi-player-screenshot.png -------------------------------------------------------------------------------- /src/utils/file-utils/types.ts: -------------------------------------------------------------------------------- 1 | /** Data used to construct files and directories. */ 2 | export type FileArray = [string, string | undefined][]; 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | * text eol=lf 4 | src/modules/**/*.lua linguist-vendored 5 | -------------------------------------------------------------------------------- /src/utils/file-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * as makeUtils from "./make-utils"; 2 | export * as pathUtils from "./path-utils"; 3 | export type { FileArray } from "./types"; 4 | -------------------------------------------------------------------------------- /src/utils/fetch-github-release/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getReleases"; 2 | export * from "./downloadRelease"; 3 | export * from "./identify"; 4 | export type { Release, Author, Asset, FetchInfo } from "./types"; 5 | -------------------------------------------------------------------------------- /src/modules/services/init.lua: -------------------------------------------------------------------------------- 1 | return setmetatable({}, { 2 | __index = function(self, serviceName) 3 | local service = game:GetService(serviceName) 4 | self[serviceName] = service 5 | return service 6 | end, 7 | }) 8 | -------------------------------------------------------------------------------- /src/api/compatibility.ts: -------------------------------------------------------------------------------- 1 | // Makes an HTTP request 2 | export const httpRequest = request || syn.request || http.request; 3 | 4 | // Gets an asset by moving it to Roblox's content folder 5 | export const getContentId = getcustomasset || getsynasset; 6 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { build } from "./build"; 2 | export { Store } from "./Store"; 3 | 4 | export { Session } from "./Session"; 5 | export { VirtualScript } from "./VirtualScript"; 6 | 7 | export type { VirtualEnvironment, Executor } from "./types"; 8 | -------------------------------------------------------------------------------- /src/modules/object-utils/README.md: -------------------------------------------------------------------------------- 1 | # @rbxts/object-utils 2 | 3 | Polyfills for Object functions 4 | 5 | ```TS 6 | import Object from "@rbxts/object-utils"; 7 | 8 | // now use Object like you could before! 9 | const v = Object.assign({}, { x: 1 }, { y: 2 }, { z: 3 }); 10 | print(v.x, v.y, v.z); 11 | ``` 12 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ESLint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install modules 15 | run: npm install 16 | - name: Run ESLint 17 | run: npx eslint src 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 4 | "editor.formatOnSave": true 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 8 | "editor.formatOnSave": true 9 | }, 10 | "eslint.run": "onType", 11 | "eslint.format.enable": true, 12 | "typescript.tsdk": "node_modules/typescript/lib" 13 | } -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRequest } from "api"; 2 | 3 | /** Sends an HTTP GET request. */ 4 | export const get = Promise.promisify((url: string) => game.HttpGetAsync(url)); 5 | 6 | /** Sends an HTTP POST request. */ 7 | export const post = Promise.promisify((url: string) => game.HttpPostAsync(url)); 8 | 9 | /** Makes an HTTP request. */ 10 | export const request = Promise.promisify(httpRequest); 11 | -------------------------------------------------------------------------------- /src/modules/zzlib/index.d.ts: -------------------------------------------------------------------------------- 1 | /** Modified version of a Lua zip library. */ 2 | interface zzlib { 3 | /** 4 | * Unzips the given zip data and returns a map of files and their contents. 5 | */ 6 | readonly unzip: (buf: string) => ZipData; 7 | } 8 | 9 | /** A container for all files and file contents in a zip file. */ 10 | type ZipData = Map; 11 | 12 | declare const zzlib: zzlib; 13 | 14 | export = zzlib; 15 | -------------------------------------------------------------------------------- /src/modules/services/README.md: -------------------------------------------------------------------------------- 1 | # @rbxts/services 2 | A module that exports common Roblox services. 3 | 4 | ```TS 5 | import { Workspace, Players, ReplicatedStorage } from "@rbxts/services"; 6 | // do something with Workspace, Players, or ReplicatedStorage 7 | ``` 8 | 9 | [You can find a list of available services here.](https://github.com/roblox-ts/services/blob/master/index.d.ts) 10 | 11 | Feel free to submit an issue if there's something missing. 12 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | Deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.x 15 | - name: Deploy docs 16 | run: | 17 | pip install mkdocs-material 18 | pip install -r docs/requirements.txt 19 | mkdocs gh-deploy --force 20 | -------------------------------------------------------------------------------- /docs/api-reference/overview.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | Rostruct is a file execution library, designed for Roblox exploiting with [Rojo](https://rojo.space/). 4 | Here, you can find documentation for Rostruct's features. 5 | 6 | ## Get started 7 | 8 | Jump directly to the documentation for some common APIs: 9 | 10 | ### Rostruct 11 | 12 | * [open](rostruct/open.md) 13 | * [fetch](rostruct/fetch.md) 14 | 15 | ### Package 16 | 17 | * [Properties](package/properties.md) 18 | * [build](package/build.md) 19 | * [require](package/require.md) 20 | -------------------------------------------------------------------------------- /docs/api-reference/rostruct/open.md: -------------------------------------------------------------------------------- 1 | # open 2 | 3 | ``` ts 4 | function open(rootDirectory: string): Package 5 | ``` 6 | 7 | Constructs a new [`Package`](../package/properties.md) object from the given folder. 8 | 9 | --- 10 | 11 | ## Parameters 12 | 13 | * `#!ts rootDirectory: string` - A path to the Roblox project 14 | 15 | --- 16 | 17 | ## Example usage 18 | 19 | ``` lua 20 | local package = Rostruct.open("projects/MyProject/") 21 | 22 | package:build("src/", { Name = "MyProject" }) 23 | 24 | package:start() 25 | ``` 26 | -------------------------------------------------------------------------------- /img/Rostruct.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/api-reference/rostruct/clearcache.md: -------------------------------------------------------------------------------- 1 | # clearCache 2 | 3 | ``` ts 4 | function clearCache() 5 | ``` 6 | 7 | Clears the Rostruct GitHub Release cache that is used by the `fetch` and `fetchLatest` functions. 8 | 9 | !!! danger "Don't use this in your projects!" 10 | This function should be used by the end user for troubleshooting only. 11 | 12 | --- 13 | 14 | ## Example usage 15 | 16 | Because the cache is cleared, this code always redownloads Roact: 17 | 18 | ``` lua 19 | Rostruct.clearCache() 20 | 21 | local package = Rostruct.fetch("Roblox", "roact", "v1.4.0") 22 | ``` 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // required 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "jsx": "react", 7 | "jsxFactory": "Roact.createElement", 8 | "jsxFragmentFactory": "Roact.Fragment", 9 | "module": "commonjs", 10 | "moduleResolution": "Node", 11 | "noLib": true, 12 | "resolveJsonModule": true, 13 | "strict": true, 14 | "target": "ESNext", 15 | "typeRoots": [ 16 | "node_modules/@rbxts" 17 | ], 18 | // configurable 19 | "rootDir": "src", 20 | "outDir": "out", 21 | "baseUrl": "src", 22 | "incremental": true, 23 | "tsBuildInfoFile": "out/tsconfig.tsbuildinfo", 24 | "declaration": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/featured/community.md: -------------------------------------------------------------------------------- 1 | # Featured 2 | 3 | To get a better understanding of Rostruct, seeing real-world examples may help. 4 | 5 | ## How to get featured 6 | 7 | If you'd like your script featured here, contact `0866#3049` on Discord, or send a PM to 0866 on V3rmillion :pray_tone4: 8 | 9 | ## Community 10 | 11 | ### [MidiPlayer](https://github.com/richie0866/MidiPlayer) by 0866 12 | 13 | A midi file auto-player for Roblox piano games. 14 | 15 | !!! success "Notable features" 16 | 17 | * UI stored in a `*.rbxm` file 18 | * A single LocalScript initializes the code 19 | * Uses common utility modules 20 | 21 | !!! example 22 | ![MidiPlayer UI](../assets/images/midi-player-screenshot.png) 23 | -------------------------------------------------------------------------------- /src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { makeUtils } from "utils/file-utils"; 2 | 3 | /** Assigns common folders to a keyword. */ 4 | const Shortcut = { 5 | ROOT: "rostruct/", 6 | CACHE: "rostruct/cache/", 7 | RELEASE_CACHE: "rostruct/cache/releases/", 8 | RELEASE_TAGS: "rostruct/cache/release_tags.json", 9 | } as const; 10 | 11 | type Shortcut = typeof Shortcut; 12 | 13 | /** Gets a Rostruct path from a keyword. */ 14 | export const getRostructPath = (keyword: T): Shortcut[T] => Shortcut[keyword]; 15 | 16 | /** Sets up core files for Rostruct. */ 17 | export const bootstrap = () => 18 | makeUtils.makeFiles([ 19 | ["rostruct/cache/releases/", ""], 20 | ["rostruct/cache/release_tags.json", "{}"], 21 | ]); 22 | -------------------------------------------------------------------------------- /src/utils/fetch-github-release/identify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a string identifier for the given download configuration. 3 | * @param owner The owner of the repository. 4 | * @param repo The repository name. 5 | * @param tag Optional release tag. Defaults to `"LATEST"`. 6 | * @param asset Optional release asset file. Defaults to `"ZIPBALL"`. 7 | * @returns An identifier for the given parameters. 8 | */ 9 | export function identify(owner: string, repo: string, tag?: string, asset?: string): string { 10 | const template = "%s-%s-%s-%s"; 11 | return template.format( 12 | owner.lower(), 13 | repo.lower(), 14 | tag !== undefined ? tag.lower() : "LATEST", 15 | asset !== undefined ? asset.lower() : "ZIPBALL", 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/core/build/txt.ts: -------------------------------------------------------------------------------- 1 | import Make from "modules/make"; 2 | import { pathUtils } from "utils/file-utils"; 3 | import { fileMetadata } from "./metadata"; 4 | 5 | /** 6 | * Transforms a plain text file into a Roblox StringValue. 7 | * @param path A path to the text file. 8 | * @param name The name of the instance. 9 | * @returns A StringValue object. 10 | */ 11 | export function makePlainText(path: string, name: string) { 12 | const stringValue = Make("StringValue", { Name: name, Value: readfile(path) }); 13 | 14 | // Applies an adjacent meta file if it exists. 15 | const metaPath = pathUtils.getParent(path) + name + ".meta.json"; 16 | if (isfile(metaPath)) fileMetadata(metaPath, stringValue); 17 | 18 | return stringValue; 19 | } 20 | -------------------------------------------------------------------------------- /src/core/build/dir.ts: -------------------------------------------------------------------------------- 1 | import Make from "modules/make"; 2 | import { pathUtils } from "utils/file-utils"; 3 | import { directoryMetadata } from "./metadata"; 4 | 5 | /** 6 | * Transforms a directory into a Roblox folder. 7 | * If an `init.meta.json` file exists, create an Instance from the file. 8 | * @param path A path to the directory. 9 | * @returns A Folder object, or an object created by a meta file. 10 | */ 11 | export function makeDir(path: string): Folder | CreatableInstances[keyof CreatableInstances] { 12 | const metaPath = path + "init.meta.json"; 13 | 14 | if (isfile(metaPath)) return directoryMetadata(metaPath, pathUtils.getName(path)); 15 | 16 | return Make("Folder", { 17 | Name: pathUtils.getName(path), 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/core/Store.ts: -------------------------------------------------------------------------------- 1 | type Store = Map; 2 | type Stores = Map; 3 | 4 | // Ensures that stores persist between sessions. 5 | const stores = 6 | getgenv().RostructStore !== undefined 7 | ? (getgenv().RostructStore as Stores) 8 | : (getgenv().RostructStore = new Map()); 9 | 10 | /** Stores persistent data between sessions. */ 11 | export const Store = { 12 | /** 13 | * Lazily creates a Map object. The map is shared between all sessions of 14 | * the current game. 15 | * @param name The name of the store. 16 | * @returns A Map object. 17 | */ 18 | getStore(storeName: string): Map { 19 | if (stores.has(storeName)) return stores.get(storeName) as Map; 20 | const store = new Map(); 21 | stores.set(storeName, store); 22 | return store; 23 | }, 24 | } as const; 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Install NodeJS 15 | uses: actions/setup-node@master 16 | with: 17 | node-version: 14.x 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Install node_modules 21 | run: | 22 | npm install cross-env 23 | npm install 24 | 25 | - name: Build Rostruct 26 | run: | 27 | npm run build:luau 28 | 29 | - name: Package Rostruct 30 | run: | 31 | npm run build:prod 32 | 33 | - name: Upload to release 34 | uses: AButler/upload-release-assets@v2.0 35 | with: 36 | files: 'Rostruct.lua' 37 | repo-token: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | /** A function that gets called when a VirtualScript is executed. */ 2 | export type Executor = (globals: VirtualEnvironment) => unknown; 3 | 4 | /** Base environment for VirtualScript instances. */ 5 | export interface VirtualEnvironment { 6 | /** A reference to the script object that a file is being executed for. */ 7 | script: LuaSourceContainer; 8 | 9 | /** 10 | * Runs the supplied `ModuleScript` if it has not been run already, and returns what the ModuleScript returned (in both cases). 11 | * 12 | * If the object is bound to a `VirtualScript`, it returns what the VirtualScript returned. 13 | */ 14 | require: (obj: ModuleScript) => unknown; 15 | 16 | /** A reference to the path of the file that is being executed. */ 17 | _PATH: string; 18 | 19 | /** A reference to the root directory `Reconciler.reify` was originally called with. */ 20 | _ROOT: string; 21 | } 22 | -------------------------------------------------------------------------------- /docs/api-reference/globals.md: -------------------------------------------------------------------------------- 1 | # Globals 2 | 3 | Scripts executed by Rostruct have modified globals to stay consistent with how actual Roblox scripts run. 4 | 5 | For example, in Rostruct scripts, the `#!lua require()` function is modified to load the ModuleScript objects Rostruct creates, and provides a detailed error traceback for recursive `#!lua require()` calls. 6 | 7 | Global environments are modified internally with the `#!lua setfenv()` function. Rostruct also adds some extra globals for convenience: 8 | 9 | --- 10 | 11 | ## `_ROOT` 12 | 13 | ``` ts 14 | const _ROOT: string 15 | ``` 16 | 17 | A reference to the Package's [`root`](package/properties.md#root) property. 18 | 19 | --- 20 | 21 | ## `_PATH` 22 | 23 | ``` ts 24 | const _PATH: string 25 | ``` 26 | 27 | A path to the Lua file that is being executed. 28 | -------------------------------------------------------------------------------- /src/utils/replace.ts: -------------------------------------------------------------------------------- 1 | type Replacement = 2 | | ((value: string) => string | number | undefined) 3 | | Map 4 | | string 5 | | number 6 | | { [index: string]: string | number }; 7 | 8 | /** 9 | * Replaces an instance of `pattern` in `str` with `replacement`. 10 | * @param str The string to match against. 11 | * @param pattern The pattern to match. 12 | * @param repl What to replace the first instance of `pattern` with. 13 | * @returns The result of global substitution, the string matched, and the position of it. 14 | */ 15 | export function replace(str: string, pattern: string, repl: Replacement): [string, string, number, number] | undefined { 16 | const [output, count] = str.gsub(pattern, repl as Parameters[1], 1); 17 | if (count > 0) { 18 | const [i, j] = str.find(pattern) as LuaTuple<[number, number]>; 19 | return [output, str.sub(i, j), i, j]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/assets/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /src/modules/make/README.md: -------------------------------------------------------------------------------- 1 | # Make 2 | 3 | A library providing sugar for declaring Instances. Mostly self-explanatory. 4 | 5 | Usage: 6 | 7 | Just pass in the ClassName of the Instance you want `Make` to generate with an object containing the properties it should have. 8 | 9 | ```ts 10 | import Make from "make"; 11 | 12 | Make("Frame", { 13 | Active: false, 14 | Children: [ 15 | Make("ImageLabel", { 16 | Image: "", 17 | BackgroundTransparency: 0, 18 | ImageTransparency: 0.5, 19 | Children: [Make("ImageButton", {})], 20 | }), 21 | Make("ImageButton", { 22 | MouseButton1Down: (x: number, y: number) => { 23 | print(x, y); 24 | }, 25 | }), 26 | ], 27 | }); 28 | ``` 29 | 30 | Additional Implementation details: 31 | 32 | - `Children` is a whitelisted member. It expects an array of Instances which will be parented to the generated instance. 33 | - Setting an event member, like `MouseButton1Down` in the example above, will `Connect` the expected callback to the event. 34 | 35 | ###### Note: The `Parent` property is always set last. This avoids ordering bugs/inefficiency 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Richard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/extract.ts: -------------------------------------------------------------------------------- 1 | import zzlib from "modules/zzlib"; 2 | import { makeUtils, pathUtils } from "utils/file-utils"; 3 | import type { FileArray } from "utils/file-utils"; 4 | 5 | /** 6 | * Extracts files from raw zip data. 7 | * @param rawData Raw zip data. 8 | * @param target The directory to extract files to. 9 | * @param ungroup 10 | * If the zip file contains a single directory, it may be useful to ungroup the files inside. 11 | * This parameter controls whether the top-level directory is ignored. 12 | */ 13 | export function extract(rawData: string, target: string, ungroup?: boolean) { 14 | const zipData = zzlib.unzip(rawData); 15 | const fileArray: FileArray = []; 16 | 17 | // Convert the path-content map to a file array 18 | for (const [path, contents] of zipData) 19 | ungroup 20 | ? // Trim the first folder off the path if 'ungroup' is true 21 | fileArray.push([pathUtils.addTrailingSlash(target) + path.match("^[^/]*/(.*)$")[0], contents]) 22 | : fileArray.push([pathUtils.addTrailingSlash(target) + path, contents]); 23 | 24 | // Make the files at the given target 25 | makeUtils.makeFiles(fileArray); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/rostruct", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "prepublishOnly": "rbxtsc", 8 | "build:luau": "cross-env NODE_ENV=production TYPE=Luau rbxtsc --verbose --type=package", 9 | "build:prod": "chmod +x \"./bin/bundle-prod.sh\" && bash ./bin/bundle-prod.sh", 10 | "build:test": "npm run build:luau && chmod +x \"./bin/bundle-test.sh\" && bash ./bin/bundle-test.sh", 11 | "build:test:min": "chmod +x \"./bin/bundle-test-min.sh\" && bash ./bin/bundle-test-min.sh" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "files": [ 17 | "out" 18 | ], 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "devDependencies": { 23 | "@rbxts/compiler-types": "^1.1.1-types.3", 24 | "@rbxts/types": "^1.0.489", 25 | "@typescript-eslint/eslint-plugin": "^4.27.0", 26 | "@typescript-eslint/parser": "^4.25.0", 27 | "eslint": "^7.32.0", 28 | "eslint-config-prettier": "^8.3.0", 29 | "eslint-plugin-prettier": "^3.4.0", 30 | "eslint-plugin-roblox-ts": "0.0.27", 31 | "luamin": "^1.0.4", 32 | "prettier": "^2.3.0", 33 | "roblox-ts": "^1.2.6", 34 | "typescript": "^4.3.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/api-reference/package/require.md: -------------------------------------------------------------------------------- 1 | # require 2 | 3 | ``` ts 4 | function require(module: ModuleScript): Promise 5 | ``` 6 | 7 | Runs `module` if it has not been run already, and returns what the ModuleScript returned. 8 | 9 | The `module` parameter must be a ModuleScript created with the [`build`](build.md) method. 10 | 11 | The function returns a Promise object for convenience. Use the [`requireAsync`](#example-usage) function if you want to wait for the result instead. 12 | 13 | --- 14 | 15 | ## Parameters 16 | 17 | * `#!ts module: ModuleScript` - The Rostruct module to load 18 | 19 | --- 20 | 21 | ## Example usage 22 | 23 | === "require" 24 | 25 | ``` lua 26 | local package = Rostruct.open("PathTo/MyModule/") 27 | 28 | package:build("src/", { 29 | Name = "MyModule", 30 | }) 31 | 32 | package:require(package.tree.MyModule) 33 | :andThen(function(MyModule) 34 | MyModule.DoSomething() 35 | end) 36 | ``` 37 | 38 | === "requireAsync" 39 | 40 | ``` lua 41 | local package = Rostruct.open("PathTo/MyModule/") 42 | 43 | package:build("src/", { 44 | Name = "MyModule", 45 | }) 46 | 47 | local MyModule = package:requireAsync(package.tree.MyModule) 48 | 49 | MyModule.DoSomething() 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/assets/stylesheets/api-tags.css: -------------------------------------------------------------------------------- 1 | .base-tag { 2 | color: white; 3 | padding: 4px; 4 | padding-inline: 8px 8px; 5 | border-radius: 4px; 6 | vertical-align: middle; 7 | font-size: 50%; 8 | font-weight: bold; 9 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 10 | margin-left: 4px; 11 | } 12 | 13 | .static-tag { 14 | background: #8f59d6; 15 | } 16 | 17 | .static-tag::before { 18 | content: "static"; 19 | } 20 | 21 | .promise-tag { 22 | background: #d659d0; 23 | } 24 | 25 | .promise-tag::before { 26 | content: "Promise"; 27 | } 28 | 29 | .yields-tag { 30 | background: #8f59d6; 31 | } 32 | 33 | .yields-tag::before { 34 | content: "yields"; 35 | } 36 | 37 | .constructor-tag { 38 | background: rgb(214, 89, 89); 39 | } 40 | 41 | .constructor-tag::before { 42 | content: "constructor"; 43 | } 44 | 45 | .interface-tag { 46 | background: #da8146; 47 | } 48 | 49 | .interface-tag::before { 50 | content: "interface"; 51 | } 52 | 53 | .debug-tag { 54 | background: #1ba565; 55 | } 56 | 57 | .debug-tag::before { 58 | content: "debug"; 59 | } 60 | 61 | .read-only-tag { 62 | background: #1ba5a5; 63 | } 64 | 65 | .read-only-tag::before { 66 | content: "read only"; 67 | } 68 | -------------------------------------------------------------------------------- /docs/api-reference/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | Some Rostruct functions may return tables to store data in one place. The structures of these tables are documented on this page. 4 | 5 | --- 6 | 7 | ## `FetchInfo` 8 | 9 | ``` ts 10 | interface FetchInfo { 11 | /** The folder the release was saved to. */ 12 | readonly location: string; 13 | 14 | /** Whether the operation downloaded a new release. */ 15 | readonly updated: boolean; 16 | 17 | /** The owner of the repository. */ 18 | readonly owner: string; 19 | 20 | /** The name of the repository. */ 21 | readonly repo: string; 22 | 23 | /** The version of the release. */ 24 | readonly tag: string; 25 | 26 | /** The specific asset that was downloaded. */ 27 | readonly asset: "Source code" | string; 28 | } 29 | ``` 30 | 31 | Represents the status of a GitHub Release fetch operation. `FetchInfo` is used in a Package object's [`fetchInfo`](./package/properties.md#fetchinfo) property. 32 | 33 | The `owner`, `repo`, `tag`, and `asset` fields typically reference the arguments passed to a `fetch` function. 34 | 35 | If an asset isn't passed to the `fetch` function, `asset` defaults to `#!lua "Source code"`. The `tag` field also defaults to the latest stable version of the repository. 36 | -------------------------------------------------------------------------------- /src/core/build/json.ts: -------------------------------------------------------------------------------- 1 | import { Session } from "core/Session"; 2 | import { VirtualScript } from "core/VirtualScript"; 3 | import Make from "modules/make"; 4 | import { HttpService } from "modules/services"; 5 | import { pathUtils } from "utils/file-utils"; 6 | import { fileMetadata } from "./metadata"; 7 | 8 | /** 9 | * Transforms a JSON file into a Roblox module. 10 | * @param session The current session. 11 | * @param path A path to the JSON file. 12 | * @param name The name of the instance. 13 | * @returns A ModuleScript with a VirtualScript binding. 14 | */ 15 | export function makeJsonModule(session: Session, path: string, name: string): ModuleScript { 16 | const instance = Make("ModuleScript", { Name: name }); 17 | 18 | // Creates and tracks a VirtualScript object for this file. 19 | // The VirtualScript returns the decoded JSON data when required. 20 | const virtualScript = new VirtualScript(instance, path, session.root); 21 | virtualScript.setExecutor(() => HttpService.JSONDecode(virtualScript.source)); 22 | session.virtualScriptAdded(virtualScript); 23 | 24 | // Applies an adjacent meta file if it exists. 25 | const metaPath = pathUtils.getParent(path) + name + ".meta.json"; 26 | if (isfile(metaPath)) fileMetadata(metaPath, instance); 27 | 28 | return instance; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/make/init.lua: -------------------------------------------------------------------------------- 1 | -- Compiled with roblox-ts v1.1.1 2 | --[[ 3 | * 4 | * Returns a table wherein an object's writable properties can be specified, 5 | * while also allowing functions to be passed in which can be bound to a RBXScriptSignal. 6 | ]] 7 | --[[ 8 | * 9 | * Instantiates a new Instance of `className` with given `settings`, 10 | * where `settings` is an object of the form { [K: propertyName]: value }. 11 | * 12 | * `settings.Children` is an array of child objects to be parented to the generated Instance. 13 | * 14 | * Events can be set to a callback function, which will be connected. 15 | * 16 | * `settings.Parent` is always set last. 17 | ]] 18 | local function Make(className, settings) 19 | local _0 = settings 20 | local children = _0.Children 21 | local parent = _0.Parent 22 | local instance = Instance.new(className) 23 | for setting, value in pairs(settings) do 24 | if setting ~= "Children" and setting ~= "Parent" then 25 | local _1 = instance 26 | local prop = _1[setting] 27 | local _2 = prop 28 | if typeof(_2) == "RBXScriptSignal" then 29 | prop:Connect(value) 30 | else 31 | instance[setting] = value 32 | end 33 | end 34 | end 35 | if children then 36 | for _, child in ipairs(children) do 37 | child.Parent = instance 38 | end 39 | end 40 | instance.Parent = parent 41 | return instance 42 | end 43 | return Make 44 | -------------------------------------------------------------------------------- /bin/bundle-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Save newline as a tab 4 | printf -v nl '\n' 5 | 6 | # Wrap the given file in a function declaration. 7 | wrap_function_decl() { 8 | local name="$1" 9 | local file="$2" 10 | 11 | # Get file source 12 | local content=$(cat $file) 13 | 14 | # Replace '_G' variable with 'TS._G' 15 | content=${content//_G\[script\]/"TS._G[script]"} 16 | 17 | # Add tab to newlines 18 | content=${content//${nl}/$nl } 19 | 20 | echo "$nl$nl-- $file: 21 | TS.register(\"$file\", \"$name\", function() 22 | 23 | -- Setup 24 | local script = TS.get(\"$file\") 25 | 26 | -- Start of $name 27 | 28 | ${content} 29 | 30 | -- End of $name 31 | 32 | end)" 33 | } 34 | 35 | bundle=$(cat 'bin/runtime.lua') 36 | 37 | traverse() { 38 | local dir="$1" 39 | 40 | # Do files first 41 | for file in "$dir"/*; do 42 | if [ -f "$file" ]; then 43 | filename=$(basename -- "$file") 44 | extension="${filename##*.}" 45 | filename="${filename%%.*}" 46 | if [ "$extension" = 'lua' ]; then 47 | bundle+=$(wrap_function_decl "$filename" "$file") 48 | fi 49 | fi 50 | done 51 | 52 | # Do folders last 53 | for file in "$dir"/*; do 54 | if [ -d "$file" ]; then 55 | traverse "$file" 56 | fi 57 | done 58 | } 59 | 60 | traverse 'out' 61 | 62 | bundle+="$nl$nl$(cat 'bin/test.lua')" 63 | 64 | >Rostruct.lua 65 | echo "${bundle}" >>Rostruct.lua 66 | -------------------------------------------------------------------------------- /src/modules/services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "@rbxts/services", 3 | "_id": "@rbxts/services@1.1.5", 4 | "_inBundle": false, 5 | "_integrity": "sha512-5/SGYCVcdvxIM19ZFf4k9Hq5jCNLW0c/1G804yr9+KuPteVyfag4YiBgkXE3QmGx1ITixIQJJYIKv1LoueWPww==", 6 | "_location": "/@rbxts/services", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "tag", 10 | "registry": true, 11 | "raw": "@rbxts/services", 12 | "name": "@rbxts/services", 13 | "escapedName": "@rbxts%2fservices", 14 | "scope": "@rbxts", 15 | "rawSpec": "", 16 | "saveSpec": null, 17 | "fetchSpec": "latest" 18 | }, 19 | "_requiredBy": [ 20 | "#USER", 21 | "/" 22 | ], 23 | "_resolved": "https://registry.npmjs.org/@rbxts/services/-/services-1.1.5.tgz", 24 | "_shasum": "cde5d38a60f38bdf3acfc4263d702f16a238dae6", 25 | "_spec": "@rbxts/services", 26 | "_where": "C:\\Users\\richard\\Documents\\Source\\Projects\\rostruct", 27 | "author": "", 28 | "bundleDependencies": false, 29 | "deprecated": false, 30 | "description": "A module that exports common Roblox services.", 31 | "devDependencies": { 32 | "@rbxts/types": "^1.0.491" 33 | }, 34 | "files": [ 35 | "init.lua", 36 | "index.d.ts" 37 | ], 38 | "license": "ISC", 39 | "main": "init.lua", 40 | "name": "@rbxts/services", 41 | "scripts": {}, 42 | "typings": "index.d.ts", 43 | "version": "1.1.5" 44 | } 45 | -------------------------------------------------------------------------------- /docs/assets/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Inter:500,500i,600,600i%7C&display=fallback'); 2 | 3 | * { 4 | scroll-behavior: smooth; 5 | scrollbar-color: dark; 6 | color-scheme: dark; 7 | } 8 | 9 | .customcode { 10 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 11 | color: #E9EBFC; 12 | } 13 | 14 | .symbol { 15 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 16 | color: #8F92AB; 17 | white-space: pre-wrap; 18 | tab-size: 4; 19 | } 20 | 21 | .keyword { 22 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 23 | color: #a897ff; 24 | white-space: pre-wrap; 25 | tab-size: 4; 26 | } 27 | 28 | .key { 29 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 30 | color: #73DACA; 31 | white-space: pre-wrap; 32 | tab-size: 4; 33 | } 34 | 35 | .interface { 36 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 37 | color: #ffab73; 38 | white-space: pre-wrap; 39 | tab-size: 4; 40 | } 41 | 42 | .type { 43 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 44 | color: #79b3ff; 45 | white-space: pre-wrap; 46 | tab-size: 4; 47 | } 48 | 49 | .highlight { 50 | font-family: 'JetBrains Mono', 'Roboto Mono', Consolas, monospace; 51 | color: #90c9ff; 52 | white-space: pre-wrap; 53 | tab-size: 4; 54 | } 55 | -------------------------------------------------------------------------------- /docs/api-reference/package/start.md: -------------------------------------------------------------------------------- 1 | # start 2 | 3 | ``` ts 4 | function start(): Promise 5 | ``` 6 | 7 | Runs every LocalScript created with the `build` method after the next `Heartbeat` event. 8 | 9 | The function returns a Promise that resolves once every script finishes executing. The Promise cancels if any of the scripts throw an error on the **root scope**[^1], but the rest of the scripts will continue. 10 | 11 | !!! warning "Script timeout" 12 | 13 | After ten seconds of suspended execution from any script, the Promise will cancel. 14 | 15 | Code within the **root scope**[^1] of any LocalScript or ModuleScript should try to finish ASAP, and should avoid yielding if possible! 16 | 17 | --- 18 | 19 | ## Example usage 20 | 21 | ``` lua 22 | local package = Rostruct.open("PathTo/MyModule/") 23 | 24 | package:build("src/", { Name = "MyModule" }) 25 | 26 | package:start() 27 | :andThen(function(scripts) 28 | print("Scripts executed:") 29 | for _, script in ipairs(scripts) do 30 | print(script:GetFullName()) 31 | end 32 | end) 33 | :catch(function(err) 34 | if Promise.Error.isKind(err, Promise.Error.Kind.TimedOut) then 35 | warn("Script execution timed out!") 36 | else 37 | warn("Something went wrong: " .. tostring(err)) 38 | end 39 | end) 40 | ``` 41 | 42 | [^1]: Refers to the main thread the script is running in, and does not include functions spawned on different threads. 43 | -------------------------------------------------------------------------------- /docs/api-reference/package/properties.md: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | A **Package** is an object that represents a Roblox project. It exposes most of Rostruct's project building API and handles Lua script runtime. 4 | 5 | ## Properties 6 | 7 | Properties are public fields exposed by a `Package` object. 8 | 9 | For example, you can access `Package.tree` like this: 10 | 11 | ``` lua 12 | local package = Rostruct.open("MyProject") 13 | 14 | print(package.tree) --> Tree 15 | ``` 16 | 17 | --- 18 | 19 | ### `tree` 20 | 21 | ``` ts 22 | readonly tree: Folder 23 | ``` 24 | 25 | A Folder object containing all objects returned by the `Package.build` method. 26 | 27 | This property helps simplify Promise usage, since you don't need to store the result of the `Package.build` method to require a module. 28 | 29 | --- 30 | 31 | ### `root` 32 | 33 | ``` ts 34 | readonly root: string 35 | ``` 36 | 37 | A reference to the root directory of the project, which was passed into the `Rostruct.open` function, or automatically provided when fetching through GitHub. 38 | 39 | The value should *always* end with a forward slash! 40 | 41 | --- 42 | 43 | ### `fetchInfo` 44 | 45 | ``` ts 46 | readonly fetchInfo?: FetchInfo | undefined 47 | ``` 48 | 49 | An object that stores data about the last `Rostruct.fetch` or `Rostruct.fetchLatest` operation. 50 | 51 | See the [FetchInfo](../types.md#fetchinfo) documentation for more info on how it's structured. 52 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Learn how to integrate Rostruct into your workflow. 4 | 5 | !!! warning 6 | Automatically getting the latest Rostruct release is discouraged, as **breaking changes** can happen at any time. 7 | 8 | Read the latest release's description when updating Rostruct just in case you need to change your code. 9 | 10 | --- 11 | 12 | Rostruct is distributed as a Lua file. Before starting your local project, you should learn how to load Rostruct. 13 | 14 | ## with HTTP GET recommended { data-toc-label="with HTTP GET" } 15 | 16 | Rostruct can be loaded with `HttpGetAsync`: 17 | 18 | ```lua hl_lines="3" 19 | local Rostruct = loadstring(game:HttpGetAsync( 20 | "https://github.com/richie0866/Rostruct/releases/download/" 21 | .. "TAG_VERSION_HERE" 22 | .. "/Rostruct.lua" 23 | ))() 24 | ``` 25 | 26 | This will load the Rostruct script for the given [GitHub Release](https://github.com/richie0866/Rostruct/releases) **tag version**. 27 | 28 | ## with `#!lua loadfile()` 29 | 30 | If you'd rather avoid yielding, you can load Rostruct locally. 31 | 32 | To do this, save the latest `Rostruct.lua` file from the [GitHub Releases page](https://github.com/richie0866/Rostruct/releases/latest) somewhere in your executor's `workspace/` directory. 33 | 34 | You can load the Lua file with: 35 | 36 | === "loadfile" 37 | 38 | ```lua 39 | local Rostruct = loadfile("Rostruct.lua")() 40 | ``` 41 | 42 | === "loadstring-readfile" 43 | 44 | ```lua 45 | local Rostruct = loadstring(readfile("Rostruct.lua"))() 46 | ``` 47 | -------------------------------------------------------------------------------- /src/core/build/rbx-model.ts: -------------------------------------------------------------------------------- 1 | import { getContentId } from "api"; 2 | import type { Session } from "core/Session"; 3 | import { VirtualScript } from "core/VirtualScript"; 4 | 5 | type ScriptWithSource = LuaSourceContainer & { Source: string }; 6 | 7 | /** 8 | * Transforms a `.rbxm` or `.rbxmx` file into a Roblox object. 9 | * @param path A path to the model file. 10 | * @param name The name of the instance. 11 | * @returns The result of `game.GetObjects(getContentId(path))`. 12 | */ 13 | export function makeRobloxModel(session: Session, path: string, name: string): Instance { 14 | assert(getContentId, `'${path}' could not be loaded; No way to get a content id`); 15 | 16 | // A neat trick to load model files is to generate a content ID, which 17 | // moves it to Roblox's content folder, and then use it as the asset id for 18 | // for GetObjects: 19 | const tree = game.GetObjects(getContentId(path)); 20 | assert(tree.size() === 1, `'${path}' could not be loaded; Only one top-level instance is supported`); 21 | 22 | const model = tree[0] as Instance | ScriptWithSource; 23 | model.Name = name; 24 | 25 | // Create VirtualScript objects for all scripts in the model 26 | for (const obj of model.GetDescendants() as (Instance | ScriptWithSource)[]) { 27 | if (obj.IsA("LuaSourceContainer")) { 28 | session.virtualScriptAdded(new VirtualScript(obj, path, session.root, obj.Source)); 29 | } 30 | } 31 | if (model.IsA("LuaSourceContainer")) { 32 | session.virtualScriptAdded(new VirtualScript(model, path, session.root, model.Source)); 33 | } 34 | 35 | return model; 36 | } 37 | -------------------------------------------------------------------------------- /docs/api-reference/package/build.md: -------------------------------------------------------------------------------- 1 | # build 2 | 3 | ``` ts 4 | function build(fileOrFolder?: string, props?: {[prop: string]: any}): Instance 5 | ``` 6 | 7 | Constructs a new Instance from a file or folder in the `root` directory, with the properties `props`. Instances returned by this function can also be found in [`Package.tree`](properties.md#tree). 8 | 9 | If `fileOrFolder` is a string, the function transforms `#!lua Package.root .. fileOrFolder`. 10 | 11 | If `fileOrFolder` is `nil`, the function transforms the root directory. 12 | 13 | You can see how files turn into Roblox objects on the [file conversion page](../file-conversion.md). 14 | 15 | !!! tip 16 | Model files (`*.rbxm`, `*.rbxmx`) that contain LocalScript and ModuleScript instances act just like normal Rostruct scripts - but the [`_PATH`](../globals.md#_path) global points to the model file. 17 | 18 | !!! caution 19 | Avoid building the root directory if it contains files Rostruct shouldn't use. It's good practice to store your source code in a specific folder in your project. 20 | 21 | --- 22 | 23 | ## Parameters 24 | 25 | * `#!ts fileOrFolder?: string | undefined` - The file or folder to build; Defaults to the root directory 26 | * `#!ts props?: {[prop: string]: any} | undefined` - A map of properties to apply to the instance 27 | 28 | --- 29 | 30 | ## Example usage 31 | 32 | ``` lua 33 | local package = Rostruct.open("PathTo/MyProject/") 34 | 35 | package:build("src/", { 36 | Name = "MyProject", 37 | }) 38 | 39 | package:build("stringValue.txt", { 40 | Name = "MyString", 41 | Value = "Hi", 42 | }) 43 | 44 | print(package.tree.MyString.Value) --> Hi 45 | ``` 46 | -------------------------------------------------------------------------------- /src/core/build/EncodedValue/index.d.ts: -------------------------------------------------------------------------------- 1 | // TODO: Actually implement Rojo's Roblox DOM 2 | 3 | declare namespace EncodedValue { 4 | /** 5 | * Constructs a Roblox data type from the property value. 6 | * @param dataType Typically the result of `typeof()`. 7 | * @param encodedValue The encoded value to construct the type with. 8 | */ 9 | export function decode( 10 | dataType: string, 11 | encodedValue: EncodedValue, 12 | ): LuaTuple<[true, CheckableTypes] | [false, string]>; 13 | 14 | /** 15 | * Sets a Roblox object's `property` to the encoded value. 16 | * 17 | * Decodes the encoded value with `typeof(obj[property])`. 18 | * 19 | * @param obj The object to write to. 20 | * @param property The name of the property. 21 | * @param encodedValue The encoded value to pass to `decode()`. 22 | */ 23 | export function setProperty( 24 | obj: T, 25 | property: keyof WritableInstanceProperties, 26 | encodedValue: EncodedValue, 27 | ): void; 28 | 29 | /** 30 | * Calls `setProperty()` on `obj` with every key-value pair 31 | * of `properties`. 32 | * @param obj The object to write to. 33 | * @param properties An object mapping property names to encoded values. 34 | */ 35 | export function setProperties( 36 | obj: T, 37 | properties: Map, EncodedValue>, 38 | ): void; 39 | 40 | export function setModelProperties( 41 | obj: T, 42 | properties: Map, unknown>, 43 | ): void; 44 | } 45 | 46 | type EncodedValue = CheckablePrimitives | CheckablePrimitives[]; 47 | 48 | export = EncodedValue; 49 | -------------------------------------------------------------------------------- /bin/bundle-prod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Save newline as a tab 4 | printf -v nl '\n' 5 | 6 | # Wrap the given file in a function declaration. 7 | wrap_function_decl() { 8 | local name="$1" 9 | local file="$2" 10 | 11 | # Get file source 12 | local content=$(cat $file) 13 | 14 | # Replace '_G' variable with 'TS._G' 15 | content=${content//_G\[script\]/"TS._G[script]"} 16 | 17 | # Add tab to newlines 18 | content=${content//${nl}/$nl } 19 | 20 | echo "${nl}TS.register(\"$file\", \"$name\", function() 21 | local script = TS.get(\"$file\") 22 | ${content} 23 | end)" 24 | } 25 | 26 | bundle=$(cat 'bin/runtime.lua') 27 | 28 | traverse() { 29 | local dir="$1" 30 | 31 | # Do files first 32 | for file in "$dir"/*; do 33 | if [ -f "$file" ]; then 34 | filename=$(basename -- "$file") 35 | extension="${filename##*.}" 36 | filename="${filename%%.*}" 37 | if [ "$extension" = 'lua' ]; then 38 | bundle+=$(wrap_function_decl "$filename" "$file") 39 | fi 40 | fi 41 | done 42 | 43 | # Do folders last 44 | for file in "$dir"/*; do 45 | if [ -d "$file" ]; then 46 | traverse "$file" 47 | fi 48 | done 49 | } 50 | 51 | # Load all Lua files 52 | traverse 'out' 53 | 54 | # End by returning the main module 55 | bundle+="${nl}return TS.initialize(\"init\")" 56 | 57 | # Clear any existing Rostruct.lua file 58 | >Rostruct.lua 59 | 60 | # Generate an output by removing all compound assignments and minifying the source 61 | output=$(echo "${bundle}" | sed -E 's/(([A-z0-9_]+\.)*[A-z_][A-z0-9_]*)\s*(\.\.|\+|\-|\*|\/|\%|\^)\=/\1 = \1 \3/g' | npx luamin -c) 62 | 63 | echo "local Rostruct = (function() ${output} end)()${nl}${nl}return Rostruct" >>Rostruct.lua 64 | -------------------------------------------------------------------------------- /src/utils/JsonStore.ts: -------------------------------------------------------------------------------- 1 | import { HttpService, RunService } from "modules/services"; 2 | 3 | interface JsonData { 4 | [key: string]: string | number | boolean | JsonData; 5 | } 6 | 7 | /** An object to read and write to JSON files. */ 8 | export class JsonStore { 9 | /** The current state of the JSON file. */ 10 | private state?: JsonData; 11 | 12 | constructor( 13 | /** A reference to the original path to the file. */ 14 | public readonly file: string, 15 | ) { 16 | assert(isfile(file), `File '${file}' must be a valid JSON file`); 17 | } 18 | 19 | /** Gets a value from the current state. */ 20 | public get(key: T): JsonData[T] { 21 | assert(this.state, "The JsonStore must be open to read from it"); 22 | return this.state[key]; 23 | } 24 | 25 | /** Gets a value from the current state. */ 26 | public set(key: T, value: JsonData[T]) { 27 | assert(this.state, "The JsonStore must be open to write to it"); 28 | this.state[key] = value; 29 | } 30 | 31 | /** Loads the state of the file. */ 32 | public open() { 33 | assert(this.state === undefined, "Attempt to open an active JsonStore"); 34 | const state = HttpService.JSONDecode(readfile(this.file)); 35 | Promise.defer((_, reject) => { 36 | if (this.state === state) { 37 | this.close(); 38 | reject("JsonStore was left open; was the thread blocked before it could close?"); 39 | } 40 | }); 41 | this.state = state; 42 | } 43 | 44 | /** Saves the current state of the file. */ 45 | public close() { 46 | assert(this.state, "Attempt to close an inactive JsonStore"); 47 | writefile(this.file, HttpService.JSONEncode(this.state)); 48 | this.state = undefined; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/fetch-github-release/downloadAsset.ts: -------------------------------------------------------------------------------- 1 | import * as http from "utils/http"; 2 | import { makeUtils } from "utils/file-utils"; 3 | import { extract } from "utils/extract"; 4 | import type { Release } from "./types"; 5 | 6 | /** 7 | * Downloads the asset file for a release. 8 | * @param release The release to get the asset from. 9 | * @param asset Optional name of the asset. If not provided, the function returns the zipball URL. 10 | * @returns The file data for an asset. 11 | */ 12 | export async function downloadAsset(release: Release, path: string, asset?: string): Promise { 13 | let assetUrl: string; 14 | 15 | // If 'asset' is specified, get the URL of the asset. 16 | if (asset !== undefined) { 17 | const releaseAsset = release.assets.find((a) => a.name === asset); 18 | assert(releaseAsset, `Release '${release.name}' does not have asset '${asset}'`); 19 | assetUrl = releaseAsset.browser_download_url; 20 | } 21 | // Otherwise, download from the source zipball. 22 | else assetUrl = release.zipball_url; 23 | 24 | const response = await http.request({ 25 | Url: assetUrl, 26 | Headers: { 27 | "User-Agent": "rostruct", 28 | }, 29 | }); 30 | 31 | assert(response.Success, response.StatusMessage); 32 | 33 | asset !== undefined && asset.match("([^%.]+)$")[0] !== "zip" 34 | ? // If 'asset' does not end with '.zip', write the contents to a file. 35 | makeUtils.makeFile(path + asset, response.Body) 36 | : // Magic boolean alert! If 'asset' is undefined, pass it to the 'ungroup' 37 | // parameter. This is because the zipball contains a folder with the source code, 38 | // and we have to ungroup this folder to extract the source files to 'path'. 39 | extract(response.Body, path, asset === undefined); 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/file-utils/path-utils.ts: -------------------------------------------------------------------------------- 1 | /** Formats the given path. **The path must be a real file or folder!** */ 2 | export function formatPath(path: string): string { 3 | assert(isfile(path) || isfolder(path), `'${path}' does not point to a folder or file`); 4 | 5 | // Replace all slashes with forward slashes 6 | path = path.gsub("\\", "/")[0]; 7 | 8 | // Add a trailing slash 9 | if (isfolder(path)) { 10 | if (path.sub(-1) !== "/") path += "/"; 11 | } 12 | 13 | return path; 14 | } 15 | 16 | /** Adds a trailing slash if there is no extension. */ 17 | export function addTrailingSlash(path: string): string { 18 | path = path.gsub("\\", "/")[0]; 19 | if (path.match("%.([^%./]+)$")[0] === undefined && path.sub(-1) !== "/") return path + "/"; 20 | else return path; 21 | } 22 | 23 | export function trimTrailingSlash(path: string): string { 24 | path = path.gsub("\\", "/")[0]; 25 | if (path.sub(-1) === "/") return path.sub(0, -2); 26 | else return path; 27 | } 28 | 29 | /** Appends a file with no extension with `.file`. */ 30 | export function addExtension(file: string): string { 31 | const hasExtension = file.reverse().match("^([^%./]+%.)")[0] !== undefined; 32 | if (!hasExtension) return file + ".file"; 33 | else return file; 34 | } 35 | 36 | /** Gets the name of a file or folder. */ 37 | export function getName(path: string): string { 38 | return path.match("([^/]+)/*$")[0] as string; 39 | } 40 | 41 | /** Returns the parent directory. */ 42 | export function getParent(path: string): string | undefined { 43 | return path.match("^(.*[/])[^/]+")[0] as string; 44 | } 45 | 46 | /** Returns the first file that exists in the directory. */ 47 | export function locateFiles(dir: string, files: string[]): string | undefined { 48 | return files.find((file: string) => isfile(dir + file)); 49 | } 50 | -------------------------------------------------------------------------------- /bin/bundle-test-min.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Save newline as a tab 4 | printf -v nl '\n' 5 | 6 | # Wrap the given file in a function declaration. 7 | wrap_function_decl() { 8 | local name="$1" 9 | local file="$2" 10 | 11 | # Get file source 12 | local content=$(cat $file) 13 | 14 | # Replace '_G' variable with 'TS._G' 15 | content=${content//_G\[script\]/"TS._G[script]"} 16 | 17 | # Add tab to newlines 18 | content=${content//${nl}/$nl } 19 | 20 | echo "${nl}TS.register(\"$file\", \"$name\", function() 21 | local script = TS.get(\"$file\") 22 | ${content} 23 | end)" 24 | } 25 | 26 | bundle=$(cat 'bin/runtime.lua') 27 | 28 | traverse() { 29 | local dir="$1" 30 | 31 | # Do files first 32 | for file in "$dir"/*; do 33 | if [ -f "$file" ]; then 34 | filename=$(basename -- "$file") 35 | extension="${filename##*.}" 36 | filename="${filename%%.*}" 37 | if [ "$extension" = 'lua' ]; then 38 | bundle+=$(wrap_function_decl "$filename" "$file") 39 | fi 40 | fi 41 | done 42 | 43 | # Do folders last 44 | for file in "$dir"/*; do 45 | if [ -d "$file" ]; then 46 | traverse "$file" 47 | fi 48 | done 49 | } 50 | 51 | # Load all Lua files 52 | traverse 'out' 53 | 54 | # End by returning the main module 55 | bundle+="${nl}return TS.initialize(\"init\")" 56 | 57 | # Generate an output by removing all compound assignments and minifying the source 58 | output=$(echo "${bundle}" | sed -E 's/(([A-z0-9_]+\.)*[A-z_][A-z0-9_]*)\s*(\.\.|\+|\-|\*|\/|\%|\^)\=/\1 = \1 \3/g' | npx luamin -c) 59 | 60 | # Finalize the test code 61 | output="local Rostruct = (function() ${output} end)()${nl}${nl}" 62 | output+="$nl$(cat 'bin/test.lua' | sed 's/local Rostruct = TS.initialize("init")//g')" 63 | 64 | # Clear any existing Rostruct.lua file 65 | >Rostruct.lua 66 | 67 | echo "$output" >>Rostruct.lua 68 | -------------------------------------------------------------------------------- /src/utils/file-utils/make-utils.ts: -------------------------------------------------------------------------------- 1 | import * as pathUtils from "./path-utils"; 2 | import type { FileArray } from "./types"; 3 | 4 | /** 5 | * Safely makes a folder by creating every parent before the final directory. 6 | * Ignores the final file if there is no trailing slash. 7 | * @param location The path of the directory to make. 8 | */ 9 | export function makeFolder(location: string) { 10 | const parts = location.split("/"); 11 | const last = parts.pop(); 12 | if (last === undefined) { 13 | return; 14 | } 15 | const parent = parts.join("/"); 16 | if (parent !== "") { 17 | makeFolder(parent); 18 | } 19 | if (!isfolder(location) && !isfile(location)) { 20 | makefolder(location); 21 | } 22 | } 23 | 24 | /** 25 | * Safely makes a file by creating every parent before the file. 26 | * Adds a `.file` extension if there is no extension. 27 | * @param location The path of the file to make. 28 | * @param content Optional file contents. 29 | */ 30 | export function makeFile(file: string, content?: string) { 31 | const parts = file.split("/"); 32 | parts.pop(); 33 | makeFolder(parts.join("/")); 34 | writefile(pathUtils.addExtension(file), content ?? ""); 35 | } 36 | 37 | /** 38 | * Safely creates files from the given list of paths. 39 | * The first string in the file array element is the path, 40 | * and the second string is the optional file contents. 41 | * @param fileArray A list of files to create and their contents. 42 | */ 43 | export function makeFiles(fileArray: FileArray) { 44 | // Create the files and directories. No sorts need to be performed because parent folders 45 | // in each path are made before the file/folder itself. 46 | for (const [path, contents] of fileArray) 47 | if (path.sub(-1) === "/" && !isfolder(path)) makeFolder(path); 48 | else if (path.sub(-1) !== "/" && !isfile(path)) makeFile(path, contents); 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/object-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "@rbxts/object-utils", 3 | "_id": "@rbxts/object-utils@1.0.4", 4 | "_inBundle": false, 5 | "_integrity": "sha512-dLLhf022ipV+9i910sOE7kl9losKHoon0WgeerHqVMQA5EYsLUsVT2AxhJuhk8MiDn5oJ2GiFofE/LadY9TpJQ==", 6 | "_location": "/@rbxts/object-utils", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "tag", 10 | "registry": true, 11 | "raw": "@rbxts/object-utils", 12 | "name": "@rbxts/object-utils", 13 | "escapedName": "@rbxts%2fobject-utils", 14 | "scope": "@rbxts", 15 | "rawSpec": "", 16 | "saveSpec": null, 17 | "fetchSpec": "latest" 18 | }, 19 | "_requiredBy": [ 20 | "#USER", 21 | "/" 22 | ], 23 | "_resolved": "https://registry.npmjs.org/@rbxts/object-utils/-/object-utils-1.0.4.tgz", 24 | "_shasum": "c53d80f89e75c401365446be63fa1396bc2cffe7", 25 | "_spec": "@rbxts/object-utils", 26 | "_where": "C:\\Users\\richard\\Documents\\Source\\Projects\\rostruct", 27 | "author": "", 28 | "bugs": { 29 | "url": "https://github.com/roblox-ts/rbxts-object-utils/issues" 30 | }, 31 | "bundleDependencies": false, 32 | "deprecated": false, 33 | "description": "Polyfills for Object functions", 34 | "devDependencies": { 35 | "@rbxts/types": "^1.0.423", 36 | "@typescript-eslint/eslint-plugin": "^4.8.2", 37 | "@typescript-eslint/parser": "^4.8.2", 38 | "eslint": "^7.14.0", 39 | "eslint-config-prettier": "^6.15.0", 40 | "eslint-plugin-prettier": "^3.1.4", 41 | "prettier": "^2.2.0", 42 | "typescript": "^4.1.2" 43 | }, 44 | "files": [ 45 | "init.lua", 46 | "index.d.ts" 47 | ], 48 | "homepage": "https://github.com/roblox-ts/rbxts-object-utils#readme", 49 | "license": "ISC", 50 | "main": "init.lua", 51 | "name": "@rbxts/object-utils", 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/roblox-ts/rbxts-object-utils.git" 55 | }, 56 | "scripts": {}, 57 | "typings": "index.d.ts", 58 | "version": "1.0.4" 59 | } 60 | -------------------------------------------------------------------------------- /docs/getting-started/execution-model.md: -------------------------------------------------------------------------------- 1 | # Execution model 2 | 3 | ## Asset management 4 | 5 | A useful pattern is to keep all assets within your project for immediate access. Let's say a project is structured like such: 6 | 7 | * src/ 8 | * Assets/ 9 | * Character.rbxm (Model) 10 | * Controllers/ 11 | * MyController.lua (ModuleScript) 12 | * Util/ 13 | * Signal.lua (ModuleScript) 14 | * Date.lua (ModuleScript) 15 | * Thread.lua (ModuleScript) 16 | * init.meta.json (Renames 'src' to 'MyProject') 17 | 18 | We can get other modules and assets in `MyController.lua` with this code: 19 | 20 | ```lua 21 | -- MyProject/Controllers/MyController.lua 22 | local myProject = script:FindFirstAncestor("MyProject") 23 | 24 | local Signal = require(myProject.Util.Signal) 25 | local Date = require(myProject.Util.Date) 26 | local Thread = require(myProject.Util.Thread) 27 | 28 | local character = myProject.Assets.Character 29 | ``` 30 | 31 | !!! tip 32 | If you need a specific file, scripts run with Rostruct contain the `_ROOT` and `_PATH` globals to access the root directory and the current file location, respectively. 33 | 34 | ## Catching Rostruct errors 35 | 36 | Functions like [`Rostruct.fetch`](../api-reference/rostruct/fetch.md) and [`Package:require`](../api-reference/package/require.md) use Promises to manage yielding and error handling. 37 | 38 | Errors thrown during runtime can be caught using the `#!lua Promise:catch()` method: 39 | 40 | ```lua 41 | package:start() 42 | :catch(function(err) 43 | if Promise.Error.isKind(err, Promise.Error.Kind.TimedOut) then 44 | warn("Script execution timed out!") 45 | else 46 | warn("Something went wrong: " .. tostring(err)) 47 | end 48 | end) 49 | ``` 50 | 51 | ## Best practices 52 | 53 | * Only one LocalScript, if any, should manage module runtime 54 | * Code should not rely on services like CollectionService that expose you to the client, so use an alternative 55 | * LocalScripts should try to finish ASAP and avoid yielding the **main thread** if possible 56 | * The codebase should never be exposed to the `game` object to prevent security vulnerabilities 57 | -------------------------------------------------------------------------------- /src/core/build/metadata.ts: -------------------------------------------------------------------------------- 1 | import Make from "modules/make"; 2 | import { HttpService } from "modules/services"; 3 | import EncodedValue from "./EncodedValue"; 4 | 5 | type CreatableInstanceName = keyof CreatableInstances; 6 | type CreatableInstance = CreatableInstances[CreatableInstanceName]; 7 | 8 | interface Metadata { 9 | className?: CreatableInstanceName; 10 | properties?: Map, EncodedValue>; 11 | } 12 | 13 | /** 14 | * Applies the given `*.meta.json` file to the `instance`. 15 | * 16 | * Note that init scripts call this function if there is 17 | * an `init.meta.json` present. 18 | * 19 | * @param metaPath A path to the meta file. 20 | * @param instance The instance to apply properties to. 21 | */ 22 | export function fileMetadata(metaPath: string, instance: Instance) { 23 | const metadata = HttpService.JSONDecode(readfile(metaPath)); 24 | 25 | // Cannot modify the className of an existing instance: 26 | assert( 27 | metadata.className === undefined, 28 | "className can only be specified in init.meta.json files if the parent directory would turn into a Folder!", 29 | ); 30 | 31 | // Uses Rojo's decoder to set properties from metadata. 32 | if (metadata.properties !== undefined) EncodedValue.setProperties(instance, metadata.properties); 33 | } 34 | 35 | /** 36 | * Creates an Instance from the given `init.meta.json` file. 37 | * 38 | * Note that this function does not get called for directories 39 | * that contain init scripts. We can assume that there are no 40 | * init scripts present. 41 | * 42 | * @param metaPath A path to the meta file. 43 | * @param name The name of the folder. 44 | * @returns A new Instance. 45 | */ 46 | export function directoryMetadata(metaPath: string, name: string): CreatableInstance { 47 | const metadata = HttpService.JSONDecode(readfile(metaPath)); 48 | 49 | // If instance isn't provided, className is never undefined. 50 | const instance = Make(metadata.className!, { Name: name }); 51 | 52 | // Uses Rojo's decoder to set properties from metadata. 53 | if (metadata.properties !== undefined) EncodedValue.setProperties(instance, metadata.properties); 54 | 55 | return instance; 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/make/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "@rbxts/make", 3 | "_id": "@rbxts/make@1.0.5", 4 | "_inBundle": false, 5 | "_integrity": "sha512-nx97nroUk+/YGVKNCHEoXBZ1BA64ZM2HSgtaxIFldWO+BwcI9CvkKvDY1O1qnzbqRFp3w/iUubqGiNViYsoZHw==", 6 | "_location": "/@rbxts/make", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "tag", 10 | "registry": true, 11 | "raw": "@rbxts/make", 12 | "name": "@rbxts/make", 13 | "escapedName": "@rbxts%2fmake", 14 | "scope": "@rbxts", 15 | "rawSpec": "", 16 | "saveSpec": null, 17 | "fetchSpec": "latest" 18 | }, 19 | "_requiredBy": [ 20 | "#USER", 21 | "/" 22 | ], 23 | "_resolved": "https://registry.npmjs.org/@rbxts/make/-/make-1.0.5.tgz", 24 | "_shasum": "faa2d5a20ac19de853f393e9f16eb84647ea3d9e", 25 | "_spec": "@rbxts/make", 26 | "_where": "C:\\Users\\richard\\Documents\\Source\\Projects\\rostruct", 27 | "author": { 28 | "name": "Validark" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/Validark/Roblox-TS-Libraries/issues" 32 | }, 33 | "bundleDependencies": false, 34 | "dependencies": { 35 | "@rbxts/compiler-types": "^1.1.1-types.3", 36 | "@rbxts/types": "^1.0.471" 37 | }, 38 | "deprecated": false, 39 | "description": "Shorthand for declaring Instances with properties.", 40 | "files": [ 41 | "init.lua", 42 | "init.d.ts", 43 | "README.md" 44 | ], 45 | "homepage": "https://github.com/Validark/Roblox-TS-Libraries/blob/master/make/README.md", 46 | "keywords": [ 47 | "resources", 48 | "roblox-typescript", 49 | "RemoteEvents", 50 | "RemoteFunctions", 51 | "Remotes", 52 | "Folder", 53 | "Manager" 54 | ], 55 | "license": "ISC", 56 | "main": "init.lua", 57 | "name": "@rbxts/make", 58 | "peerDependencies": { 59 | "@rbxts/compiler-types": "^1.1.1-types.3", 60 | "@rbxts/types": "^1.0.471" 61 | }, 62 | "publishConfig": { 63 | "access": "public" 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "https://github.com/Validark/Roblox-TS-Libraries/tree/master/make" 68 | }, 69 | "scripts": { 70 | "test": "echo \"Error: no test specified\" && exit 1" 71 | }, 72 | "types": "init.d.ts", 73 | "version": "1.0.5" 74 | } 75 | -------------------------------------------------------------------------------- /src/core/build/json-model.ts: -------------------------------------------------------------------------------- 1 | import Make from "modules/make"; 2 | import { HttpService } from "modules/services"; 3 | import EncodedValue from "./EncodedValue"; 4 | 5 | type CreatableInstanceName = keyof CreatableInstances; 6 | type CreatableInstance = CreatableInstances[CreatableInstanceName]; 7 | 8 | interface JsonModel { 9 | ClassName: CreatableInstanceName; 10 | Name?: string; 11 | Properties?: Map, EncodedValue>; 12 | Children?: JsonModel[]; 13 | } 14 | 15 | /** 16 | * Recursively generates Roblox instances from the given model data. 17 | * @param modelData The properties and children of the model. 18 | * @param path A path to the model file for debugging. 19 | * @param name The name of the model file, for the top-level instance only. 20 | * @returns An Instance created with the model data. 21 | */ 22 | function jsonModel(modelData: JsonModel, path: string, name?: string): CreatableInstance { 23 | // The 'Name' field is required for all other instances. 24 | assert(name ?? modelData.Name, `A child in the model file '${path}' is missing a Name field`); 25 | 26 | if (name !== undefined && modelData.Name !== undefined && modelData.Name !== name) 27 | warn(`The name of the model file at '${path}' (${name}) does not match the Name field '${modelData.Name}'`); 28 | 29 | // The 'ClassName' field is required. 30 | assert(modelData.ClassName !== undefined, `An object in the model file '${path}' is missing a ClassName field`); 31 | 32 | const obj = Make(modelData.ClassName, { Name: name ?? modelData.Name }); 33 | 34 | if (modelData.Properties) EncodedValue.setModelProperties(obj, modelData.Properties); 35 | 36 | if (modelData.Children) 37 | for (const entry of modelData.Children) { 38 | const child = jsonModel(entry, path); 39 | child.Parent = obj; 40 | } 41 | 42 | return obj; 43 | } 44 | 45 | /** 46 | * Transforms a JSON model file into a Roblox object. 47 | * @param path A path to the JSON file. 48 | * @param name The name of the instance. 49 | * @returns An Instance created from the JSON model file. 50 | */ 51 | export function makeJsonModel(path: string, name: string): Instance { 52 | return jsonModel(HttpService.JSONDecode(readfile(path)), path, name); 53 | } 54 | -------------------------------------------------------------------------------- /src/core/build/lua.ts: -------------------------------------------------------------------------------- 1 | import { Session } from "core/Session"; 2 | import { VirtualScript } from "core/VirtualScript"; 3 | import Make from "modules/make"; 4 | import { replace } from "utils/replace"; 5 | import { pathUtils } from "utils/file-utils"; 6 | import { fileMetadata } from "./metadata"; 7 | 8 | const TRAILING_TO_CLASS: { [trailing: string]: "Script" | "ModuleScript" | "LocalScript" } = { 9 | ".server.lua": "Script", 10 | ".client.lua": "LocalScript", 11 | ".lua": "ModuleScript", 12 | } as const; 13 | 14 | /** 15 | * Transforms a Lua file into a Roblox script. 16 | * @param session The current session. 17 | * @param path A path to the Lua file. 18 | * @param name The name of the instance. 19 | * @returns A Lua script with a VirtualScript binding. 20 | */ 21 | export function makeLua(session: Session, path: string, nameOverride?: string): LuaSourceContainer { 22 | const fileName = pathUtils.getName(path); 23 | 24 | // Look for a name and file type that fits: 25 | const [name, match] = 26 | replace(fileName, "(%.client%.lua)$", "") || 27 | replace(fileName, "(%.server%.lua)$", "") || 28 | replace(fileName, "(%.lua)$", "") || 29 | error(`Invalid Lua file at ${path}`); 30 | 31 | // Creates an Instance for the preceding match. 32 | // If an error was not thrown, this line should always succeed. 33 | const instance = Make(TRAILING_TO_CLASS[match], { Name: nameOverride ?? name }); 34 | 35 | // Create and track a VirtualScript object for this file: 36 | session.virtualScriptAdded(new VirtualScript(instance, path, session.root)); 37 | 38 | // Applies an adjacent meta file if it exists. 39 | // This includes init.meta.json files! 40 | const metaPath = pathUtils.getParent(path) + name + ".meta.json"; 41 | if (isfile(metaPath)) fileMetadata(metaPath, instance); 42 | 43 | return instance; 44 | } 45 | 46 | /** 47 | * Transforms the parent directory into a Roblox script. 48 | * @param session The current session. 49 | * @param path A path to the `init.*.lua` file. 50 | * @param name The name of the instance. 51 | * @returns A Lua script with a VirtualScript binding. 52 | */ 53 | export function makeLuaInit(session: Session, path: string): Instance { 54 | // The parent directory will always exist for an init file. 55 | const parentDir = pathUtils.getParent(path)!; 56 | const instance = makeLua(session, path, pathUtils.getName(parentDir)); 57 | 58 | return instance; 59 | } 60 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "jsx": true, 5 | "useJSXTextNode": true, 6 | "ecmaVersion": 2018, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "plugins": [ 11 | "@typescript-eslint", 12 | "roblox-ts", 13 | "prettier" 14 | ], 15 | "extends": [ 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:roblox-ts/recommended", 18 | "plugin:prettier/recommended" 19 | ], 20 | "rules": { 21 | "prettier/prettier": [ 22 | "warn", 23 | { 24 | "semi": true, 25 | "trailingComma": "all", 26 | "singleQuote": false, 27 | "printWidth": 120, 28 | "tabWidth": 4, 29 | "useTabs": true, 30 | "endOfLine": "auto" 31 | } 32 | ], 33 | "@typescript-eslint/member-ordering": [ 34 | "warn", 35 | { 36 | "default": [ 37 | // Index signature 38 | "signature", 39 | // Fields 40 | "public-static-field", 41 | "protected-static-field", 42 | "private-static-field", 43 | "public-decorated-field", 44 | "protected-decorated-field", 45 | "private-decorated-field", 46 | "public-instance-field", 47 | "protected-instance-field", 48 | "private-instance-field", 49 | "public-abstract-field", 50 | "protected-abstract-field", 51 | "private-abstract-field", 52 | "public-field", 53 | "protected-field", 54 | "private-field", 55 | "static-field", 56 | "instance-field", 57 | "abstract-field", 58 | "decorated-field", 59 | "field", 60 | // Constructors 61 | "public-constructor", 62 | "protected-constructor", 63 | "private-constructor", 64 | "constructor", 65 | // Methods 66 | "public-static-method", 67 | "protected-static-method", 68 | "private-static-method", 69 | "public-decorated-method", 70 | "protected-decorated-method", 71 | "private-decorated-method", 72 | "public-instance-method", 73 | "protected-instance-method", 74 | "private-instance-method", 75 | "public-abstract-method", 76 | "protected-abstract-method", 77 | "private-abstract-method", 78 | "public-method", 79 | "protected-method", 80 | "private-method", 81 | "static-method", 82 | "instance-method", 83 | "abstract-method", 84 | "decorated-method", 85 | "method" 86 | ] 87 | } 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/utils/fetch-github-release/getReleases.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from "modules/services"; 2 | import * as http from "utils/http"; 3 | import type { Release } from "./types"; 4 | 5 | /** 6 | * Gets a list of releases for the Github repository. 7 | * Automatically excludes drafts, but excluding prereleases is optional. 8 | * @param owner The owner of the repository. 9 | * @param repo The repository name. 10 | * @param filterRelease Function to filter the release list. 11 | * @returns A list of Releases for the Github repository. 12 | */ 13 | export async function getReleases( 14 | owner: string, 15 | repo: string, 16 | filterRelease = (release: Release) => !release.draft, 17 | ): Promise { 18 | const response = await http.request({ 19 | Url: `https://api.github.com/repos/${owner}/${repo}/releases`, 20 | Headers: { 21 | "User-Agent": "rostruct", 22 | }, 23 | }); 24 | assert(response.Success, response.StatusMessage); 25 | const releases = HttpService.JSONDecode(response.Body); 26 | return releases.filter(filterRelease); 27 | } 28 | 29 | /** 30 | * Gets a specific release for the given repository. 31 | * This function does not get prereleases! 32 | * @param owner The owner of the repository. 33 | * @param repo The repository name. 34 | * @param tag The release tag to retrieve. 35 | * @returns A list of Releases for the Github repository. 36 | */ 37 | export async function getRelease(owner: string, repo: string, tag: string): Promise { 38 | const response = await http.request({ 39 | Url: `https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`, 40 | Headers: { 41 | "User-Agent": "rostruct", 42 | }, 43 | }); 44 | assert(response.Success, response.StatusMessage); 45 | return HttpService.JSONDecode(response.Body); 46 | } 47 | 48 | /** 49 | * Gets the latest release for the given repository. 50 | * This function does not get prereleases! 51 | * @param owner The owner of the repository. 52 | * @param repo The repository name. 53 | * @returns A list of Releases for the Github repository. 54 | */ 55 | export async function getLatestRelease(owner: string, repo: string): Promise { 56 | const response = await http.request({ 57 | Url: `https://api.github.com/repos/${owner}/${repo}/releases/latest`, 58 | Headers: { 59 | "User-Agent": "rostruct", 60 | }, 61 | }); 62 | assert(response.Success, response.StatusMessage); 63 | return HttpService.JSONDecode(response.Body); 64 | } 65 | -------------------------------------------------------------------------------- /docs/api-reference/rostruct/fetch.md: -------------------------------------------------------------------------------- 1 | # fetch 2 | 3 | ``` ts 4 | function fetch(owner: string, repo: string, tag: string, asset?: string): Promise 5 | ``` 6 | 7 | Constructs a new [`Package`](../package/properties.md) object from the GitHub Release, with a defined `fetchInfo` property. 8 | 9 | When using this function, the asset gets saved to a local cache, which makes future `fetch` calls for the same asset resolve right away. Zip files are extracted using a modified version of the [zzlib](https://github.com/zerkman/zzlib) library. 10 | 11 | The function returns a Promise object for convenience. Use the [`fetchAsync`](#example-usage) function if you want to wait for the result instead. 12 | 13 | !!! warning "Fetching from a large repository" 14 | 15 | When the `asset` field is undefined, the source code of the release will be downloaded. 16 | 17 | Because Rostruct uses a Lua zip library to extract `.zip` files, there may be performance issues when extracting large files. 18 | Prefer to [upload an asset](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository) for files you want to run in Rostruct. 19 | 20 | --- 21 | 22 | ## Parameters 23 | 24 | * `#!ts owner: string` - The owner of the repository 25 | * `#!ts repo: string` - The name of the repository 26 | * `#!ts tag: string` - The tag version to download 27 | * `#!ts asset?: string | undefined` - Optional asset to download; If not specified, it downloads the source files 28 | 29 | --- 30 | 31 | ## Example usage 32 | 33 | This example loads [Roact](https://github.com/Roblox/roact) v1.4.0: 34 | 35 | === "fetch" 36 | 37 | ``` lua 38 | local Roact = Rostruct.fetch("Roblox", "roact", "v1.4.0") 39 | :andThen(function(package) 40 | if package.fetchInfo.updated then 41 | print("First time installation!") 42 | end 43 | 44 | package:build("src/", { Name = "Roact" }) 45 | 46 | return package:require(package.tree.Roact) 47 | end) 48 | :catch(function(err) 49 | warn("Error loading Roact:", err) 50 | end) 51 | :expect() 52 | ``` 53 | 54 | === "fetchAsync" 55 | 56 | ``` lua 57 | local package = Rostruct.fetchAsync("Roblox", "roact", "v1.4.0") 58 | 59 | if package.fetchInfo.updated then 60 | print("First time installation!") 61 | end 62 | 63 | package:build("src/", { Name = "Roact" }) 64 | 65 | local Roact = package:requireAsync(package.tree.Roact) 66 | ``` 67 | -------------------------------------------------------------------------------- /src/modules/services/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare const AnalyticsService: AnalyticsService; 2 | export declare const AssetService: AssetService; 3 | export declare const BadgeService: BadgeService; 4 | export declare const Chat: Chat; 5 | export declare const CollectionService: CollectionService; 6 | export declare const ContentProvider: ContentProvider; 7 | export declare const ContextActionService: ContextActionService; 8 | export declare const ControllerService: ControllerService; 9 | export declare const DataStoreService: DataStoreService; 10 | export declare const Debris: Debris; 11 | export declare const GamePassService: GamePassService; 12 | export declare const GroupService: GroupService; 13 | export declare const GuiService: GuiService; 14 | export declare const HapticService: HapticService; 15 | export declare const HttpService: HttpService; 16 | export declare const InsertService: InsertService; 17 | export declare const JointsService: JointsService; 18 | export declare const Lighting: Lighting; 19 | export declare const LocalizationService: LocalizationService; 20 | export declare const LogService: LogService; 21 | export declare const MarketplaceService: MarketplaceService; 22 | export declare const MessagingService: MessagingService; 23 | export declare const PathfindingService: PathfindingService; 24 | export declare const PhysicsService: PhysicsService; 25 | export declare const Players: Players; 26 | export declare const PolicyService: PolicyService; 27 | export declare const ProximityPromptService: ProximityPromptService; 28 | export declare const ReplicatedFirst: ReplicatedFirst; 29 | export declare const ReplicatedStorage: ReplicatedStorage; 30 | export declare const RunService: RunService; 31 | export declare const ScriptContext: ScriptContext; 32 | export declare const ServerScriptService: ServerScriptService; 33 | export declare const ServerStorage: ServerStorage; 34 | export declare const SocialService: SocialService; 35 | export declare const SoundService: SoundService; 36 | export declare const StarterGui: StarterGui; 37 | export declare const StarterPack: StarterPack; 38 | export declare const StarterPlayer: StarterPlayer; 39 | export declare const Stats: Stats; 40 | export declare const Teams: Teams; 41 | export declare const TeleportService: TeleportService; 42 | export declare const TextService: TextService; 43 | export declare const TweenService: TweenService; 44 | export declare const UserInputService: UserInputService; 45 | export declare const UserService: UserService; 46 | export declare const VRService: VRService; 47 | export declare const Workspace: Workspace; 48 | -------------------------------------------------------------------------------- /docs/api-reference/rostruct/fetchlatest.md: -------------------------------------------------------------------------------- 1 | # fetchLatest 2 | 3 | ``` ts 4 | function fetchLatest(owner: string, repo: string, asset?: string): Promise 5 | ``` 6 | 7 | Constructs a new [`Package`](../package/properties.md) object from the latest **stable** GitHub Release, with a defined `fetchInfo` property. 8 | 9 | When using this function, the asset gets saved to a local cache. However, this function will *always* make an HTTP request to get the latest release tag. Zip files are extracted using a modified version of the [zzlib](https://github.com/zerkman/zzlib) library. 10 | 11 | Unlike [`Rostruct.fetch`](./fetch.md), this function does not load release drafts or prereleases. 12 | 13 | The function returns a Promise object for convenience. Use the [`fetchLatestAsync`](#example-usage) function if you want to wait for the result instead. 14 | 15 | !!! warning "Fetching from a large repository" 16 | 17 | When the `asset` field is undefined, the source code of the release will be downloaded. 18 | 19 | Because Rostruct uses a Lua zip library to extract `.zip` files, there may be performance issues when extracting large files. 20 | Prefer to [upload an asset](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository) for files you want to run in Rostruct. 21 | 22 | --- 23 | 24 | ## Parameters 25 | 26 | * `#!ts owner: string` - The owner of the repository 27 | * `#!ts repo: string` - The name of the repository 28 | * `#!ts asset?: string | undefined` - Optional asset to download; If not specified, it downloads the source files 29 | 30 | --- 31 | 32 | ## Example usage 33 | 34 | This example loads the latest stable release of [Roact](https://github.com/Roblox/roact): 35 | 36 | === "fetchLatest" 37 | 38 | ``` lua 39 | local Roact = Rostruct.fetchLatest("Roblox", "roact") 40 | :andThen(function(package) 41 | if package.fetchInfo.updated then 42 | print("Upgraded to version " .. package.fetchInfo.tag) 43 | end 44 | 45 | package:build("src/", { Name = "Roact" }) 46 | 47 | return package:require(package.tree.Roact) 48 | end) 49 | :catch(function(err) 50 | warn("Error loading Roact:", err) 51 | end) 52 | :expect() 53 | ``` 54 | 55 | === "fetchLatestAsync" 56 | 57 | ``` lua 58 | local package = Rostruct.fetchLatestAsync("Roblox", "roact") 59 | 60 | if package.fetchInfo.updated then 61 | print("Upgraded to version " .. package.fetchInfo.tag) 62 | end 63 | 64 | package:build("src/", { Name = "Roact" }) 65 | 66 | local Roact = package:requireAsync(package.tree.Roact) 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/getting-started/using-other-projects.md: -------------------------------------------------------------------------------- 1 | # Using other projects 2 | 3 | !!! note 4 | 5 | This page is mainly for loading Rostruct projects **in another project**, and not in a single file. 6 | 7 | In a project distributed as a single script, you should load multi-file dependencies using the functions Rostruct provides, like [`fetch`](../api-reference/rostruct/fetch.md) and [`fetchLatest`](../api-reference/rostruct/fetchlatest.md). 8 | 9 | Using resources like libraries and utility modules in your projects can make development easier. However, resources aren't always distributed as a single Lua script. 10 | 11 | For example, a UI library could be released as a Rostruct project that loads itself like this: 12 | 13 | ```lua 14 | local Rostruct = loadstring(game:HttpGetAsync( 15 | "https://github.com/richie0866/Rostruct/releases/download/v1.2.3/Rostruct.lua" 16 | ))() 17 | 18 | local package = Rostruct.fetchAsync("stickmasterluke", "MyModule") 19 | local myModule = package:build("src/", { Name = "MyModule" }) 20 | 21 | return package:requireAsync(myModule) 22 | ``` 23 | 24 | This is valid code in a Rostruct project. However, Rostruct is early in development, and may have unwanted side effects when using it *inside* a Rostruct project. This might change in the future, though. 25 | 26 | Exercise caution when using Rostruct in a Rostruct project, or opt for another solution: 27 | 28 | ## Download it manually 29 | 30 | !!! warning 31 | 32 | This method to load dependencies may be deprecated in favor of using Rostruct internally, so stay notified by watching the GitHub repository. 33 | 34 | One way to load a dependency in your project is to include their source files in your codebase. You can download it with these steps: 35 | 36 | 1. Download the project's latest GitHub Release 37 | - If their [retriever](publishing-your-project.md#deploying-from-github) fetches a specific asset, then download that asset 38 | 2. Move the source somewhere in your project 39 | - If the source folder needs specific properties before runtime, use an [`init.meta.json`](creating-your-project.md#setting-build-metadata) file to set the properties, if not already provided. 40 | - If only the name is changed, change the name of the directory in your project files. 41 | 3. Use `#!lua require()` to load the module in your project 42 | 43 | Once you've set up the files, Rostruct turns the dependencies into instances with the rest of your project, ensuring immediate access to them. You should use the global `#!lua require()` function to load them: 44 | 45 | ```lua hl_lines="3" 46 | local myProject = script:FindFirstAncestor("MyProject") 47 | 48 | local Roact = require(myProject.Modules.Roact) 49 | 50 | local character = myProject.Assets.Character 51 | ``` 52 | -------------------------------------------------------------------------------- /src/core/Session.ts: -------------------------------------------------------------------------------- 1 | import { Store } from "./Store"; 2 | import { HttpService } from "modules/services"; 3 | import { VirtualScript } from "./VirtualScript"; 4 | import { build as buildRoblox } from "./build"; 5 | 6 | /** Class used to transform files into a Roblox instance tree. */ 7 | export class Session { 8 | /** List of Session objects for a given session id. */ 9 | private static readonly sessions = Store.getStore("Sessions"); 10 | 11 | /** An identifier used to reference a Session without passing it. */ 12 | public readonly sessionId = HttpService.GenerateGUID(false); 13 | 14 | /** Store VirtualScript objects created for this Session. */ 15 | private readonly virtualScripts = new Array(); 16 | 17 | constructor( 18 | /** The directory to turn into an instance tree. */ 19 | public readonly root: string, 20 | ) { 21 | Session.sessions.set(this.sessionId, this); 22 | } 23 | 24 | /** 25 | * Gets a Session object for the given session id. 26 | * @param sessionId 27 | * @returns A Session object if it exists. 28 | */ 29 | public static fromSessionId(sessionId: string): Session | undefined { 30 | return this.sessions.get(sessionId); 31 | } 32 | 33 | /** 34 | * Stores a VirtualScript object. 35 | * @param virtualScript A new VirtualScript object. 36 | */ 37 | public virtualScriptAdded(virtualScript: VirtualScript) { 38 | this.virtualScripts.push(virtualScript); 39 | } 40 | 41 | /** 42 | * Turns descendants of the root directory into Roblox objects. 43 | * If `path` is not provided, this function transforms the root directory. 44 | * @param path Optional descendant to build in the root directory. 45 | * @returns A Roblox object for the root directory. 46 | */ 47 | public build(path = ""): Instance | undefined { 48 | assert( 49 | isfile(this.root + path) || isfolder(this.root + path), 50 | `The path '${this.root + path}' must be a file or folder`, 51 | ); 52 | 53 | return buildRoblox(this, this.root + path); 54 | } 55 | 56 | /** 57 | * Runs every virtual LocalScript for this session on deferred threads. 58 | * @returns 59 | * A promise that resolves with an array of scripts that finished executing. 60 | * If one script throws an error, the entire promise will cancel. 61 | */ 62 | public simulate(): Promise { 63 | const executingPromises: Promise[] = []; 64 | 65 | assert(this.virtualScripts.size() > 0, "This session cannot start because no LocalScripts were found."); 66 | 67 | for (const v of this.virtualScripts) 68 | if (v.instance.IsA("LocalScript")) 69 | executingPromises.push(v.deferExecutor().andThenReturn(v.instance as LocalScript)); 70 | 71 | return Promise.all[]>(executingPromises).timeout(10); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/modules/object-utils/init.lua: -------------------------------------------------------------------------------- 1 | local HttpService = game:GetService("HttpService") 2 | 3 | local Object = {} 4 | 5 | function Object.keys(object) 6 | local result = table.create(#object) 7 | for key in pairs(object) do 8 | result[#result + 1] = key 9 | end 10 | return result 11 | end 12 | 13 | function Object.values(object) 14 | local result = table.create(#object) 15 | for _, value in pairs(object) do 16 | result[#result + 1] = value 17 | end 18 | return result 19 | end 20 | 21 | function Object.entries(object) 22 | local result = table.create(#object) 23 | for key, value in pairs(object) do 24 | result[#result + 1] = { key, value } 25 | end 26 | return result 27 | end 28 | 29 | function Object.assign(toObj, ...) 30 | for i = 1, select("#", ...) do 31 | local arg = select(i, ...) 32 | if type(arg) == "table" then 33 | for key, value in pairs(arg) do 34 | toObj[key] = value 35 | end 36 | end 37 | end 38 | return toObj 39 | end 40 | 41 | function Object.copy(object) 42 | local result = table.create(#object) 43 | for k, v in pairs(object) do 44 | result[k] = v 45 | end 46 | return result 47 | end 48 | 49 | local function deepCopyHelper(object, encountered) 50 | local result = table.create(#object) 51 | encountered[object] = result 52 | 53 | for k, v in pairs(object) do 54 | if type(k) == "table" then 55 | k = encountered[k] or deepCopyHelper(k, encountered) 56 | end 57 | 58 | if type(v) == "table" then 59 | v = encountered[v] or deepCopyHelper(v, encountered) 60 | end 61 | 62 | result[k] = v 63 | end 64 | 65 | return result 66 | end 67 | 68 | function Object.deepCopy(object) 69 | return deepCopyHelper(object, {}) 70 | end 71 | 72 | function Object.deepEquals(a, b) 73 | -- a[k] == b[k] 74 | for k in pairs(a) do 75 | local av = a[k] 76 | local bv = b[k] 77 | if type(av) == "table" and type(bv) == "table" then 78 | local result = Object.deepEquals(av, bv) 79 | if not result then 80 | return false 81 | end 82 | elseif av ~= bv then 83 | return false 84 | end 85 | end 86 | 87 | -- extra keys in b 88 | for k in pairs(b) do 89 | if a[k] == nil then 90 | return false 91 | end 92 | end 93 | 94 | return true 95 | end 96 | 97 | function Object.toString(data) 98 | return HttpService:JSONEncode(data) 99 | end 100 | 101 | function Object.isEmpty(object) 102 | return next(object) == nil 103 | end 104 | 105 | function Object.fromEntries(entries) 106 | local entriesLen = #entries 107 | 108 | local result = table.create(entriesLen) 109 | if entries then 110 | for i = 1, entriesLen do 111 | local pair = entries[i] 112 | result[pair[1]] = pair[2] 113 | end 114 | end 115 | return result 116 | end 117 | 118 | return Object 119 | -------------------------------------------------------------------------------- /docs/api-reference/file-conversion.md: -------------------------------------------------------------------------------- 1 | # File conversion 2 | 3 | ??? example 4 | 5 | ![MyModule source](../assets/images/midi-player-src.png){ align=middle width=256px } 6 | -> [`Package.build`](../api-reference/package/methods/../build.md) -> 7 | ![MyModule instance](../assets/images/midi-player-panel.svg){ align=middle width=256px } 8 | 9 | ???+ danger 10 | 11 | Because Rostruct runs code, and Rojo syncs code to Roblox Studio, some key differences exist in functionality. 12 | 13 | !!! missing "Not supported" 14 | 15 | * Rojo project files 16 | * Project files structure your codebase around the `game` object, which would expose your project to the client. 17 | 18 | !!! warning "Differences" 19 | 20 | * `.rbxm` and `.rbxmx` files are fully supported, but [the former](https://rojo.space/docs/6.x/sync-details/#models) is buggy in Rojo. 21 | 22 | !!! bug "Known issues" 23 | 24 | * `*.model.json` files do not support Rojo's custom properties like `Instance.Tags` and `LocalizationTable.Content`. 25 | * `*.meta.json` files infer property types differently than Rojo meta files. 26 | 27 | Rostruct file conversion mirrors [Rojo's sync details](https://rojo.space/docs/6.x/sync-details/). 28 | 29 | Concepts on the table below will redirect you to their respective Rojo pages. 30 | 31 | ## Supported Rojo concepts 32 | 33 | | Concept | File Name | Supported | 34 | | ------------------------------------------------------------------------------------ | ---------------- | :--------------: | 35 | | [Folders](https://rojo.space/docs/6.x/sync-details/#folders) | any directory | :material-check: | 36 | | Server [scripts](https://rojo.space/docs/6.x/sync-details/#scripts) | `*.server.lua` | :material-minus: | 37 | | Client [scripts](https://rojo.space/docs/6.x/sync-details/#scripts) | `*.client.lua` | :material-check: | 38 | | Module [scripts](https://rojo.space/docs/6.x/sync-details/#scripts) | `*.lua` | :material-check: | 39 | | XML [models](https://rojo.space/docs/6.x/sync-details/#models) | `*.rbxmx` | :material-check: | 40 | | Binary [models](https://rojo.space/docs/6.x/sync-details/#models) | `*.rbxm` | :material-check: | 41 | | [Localization tables](https://rojo.space/docs/6.x/sync-details/#localization-tables) | `*.csv` | :material-check: | 42 | | [Plain text](https://rojo.space/docs/6.x/sync-details/#plain-text) | `*.txt` | :material-check: | 43 | | [JSON modules](https://rojo.space/docs/6.x/sync-details/#json-modules) | `*.json` | :material-check: | 44 | | [JSON models](https://rojo.space/docs/6.x/sync-details/#json-models) | `*.model.json` | :material-check: | 45 | | [Projects](https://rojo.space/docs/6.x/sync-details/#projects) | `*.project.json` | :material-close: | 46 | | [Meta files](https://rojo.space/docs/6.x/sync-details/#json-modules) | `*.meta.json` | :material-minus: | 47 | -------------------------------------------------------------------------------- /docs/getting-started/creating-your-project.md: -------------------------------------------------------------------------------- 1 | # Creating your project 2 | 3 | Rostruct turns your Lua projects into Roblox objects and handles script runtime for you. Essentially, it's like [Rojo](https://rojo.space/), but for Roblox script execution. 4 | 5 | So, before you start, remember that you can safely use the `script` and `#!lua require()` globals in your project. Every Lua script is loaded with a [modified global environment](../api-reference/globals.md), making it nearly identical to running a LocalScript or ModuleScript. See the [execution model](execution-model.md) for an example with asset management. 6 | 7 | ## Setup 8 | 9 | To set up a project, locate your executor's `workspace/` directory and create a folder somewhere to host your project. 10 | 11 | You can initialize a project with Rojo. However, if you don't have Rojo, you should create a folder that stores the source code of your project, and that's all you need to start coding. 12 | 13 | ## Sync to Roblox as you write 14 | 15 | With [Rojo](https://rojo.space/), your project files sync to Roblox Studio in real-time. This can be an alternative to frequently restarting Roblox to test your code. 16 | 17 | You can get [Rojo for VS Code](https://marketplace.visualstudio.com/items?itemName=evaera.vscode-rojo), which will install both the Rojo Roblox Studio plugin and the command-line interface. 18 | 19 | ## Building your project 20 | 21 | Once you're ready to test your local project, you can build it with: 22 | 23 | ``` lua hl_lines="3 4" 24 | local Rostruct -- Method to load Rostruct goes here 25 | 26 | local package = Rostruct.open("projects/MyProject/") 27 | local build = package:build("src/", { Name = "MyProject" }) 28 | ``` 29 | 30 | Then, you can run every LocalScript in the project, or require a specific module: 31 | 32 | ``` lua 33 | -- Run all LocalScripts after the next Heartbeat event 34 | package:start() 35 | 36 | -- Require a specific module 37 | local MyModule = package:require(build.MyModule) 38 | ``` 39 | 40 | For complete documentation, check out the [API reference](../api-reference/overview.md). 41 | 42 | ## Setting build metadata 43 | 44 | Some scripts need to know the top-level instance to access other objects, like this: 45 | 46 | ``` lua hl_lines="1" 47 | local myProject = script:FindFirstAncestor("MyProject") 48 | 49 | local Roact = require(myProject.Modules.Roact) 50 | 51 | local character = myProject.Assets.Character 52 | ``` 53 | 54 | Typically, in Rojo, that instance's name can be set in the `*.project.json` file. However, Rostruct does not (and likely never will!) support Rojo project files. 55 | 56 | Though this can be achieved with the `props` argument in the [`Package:build`](../api-reference/package/build.md) method, you can also use **meta files** to keep things simple. 57 | 58 | [Meta files](https://rojo.space/docs/6.x/sync-details/#meta-files) are a powerful tool from Rojo that tells Rostruct how to create the Instance for a specific file. For example, this meta file changes the name of the parent folder, `src/`: 59 | 60 | === "src/init.meta.json" 61 | 62 | ```json 63 | { 64 | "properties": { 65 | "Name": "MyProject" 66 | } 67 | } 68 | ``` 69 | 70 | For more details, see Rojo's page on [meta files](https://rojo.space/docs/6.x/sync-details/#meta-files). 71 | -------------------------------------------------------------------------------- /bin/test.lua: -------------------------------------------------------------------------------- 1 | local Rostruct = TS.initialize("init") 2 | 3 | local function assertTypes(name, t, types) 4 | for k, v in pairs(types) do 5 | local keyName = name .. "." .. k 6 | if type(v) == "table" then 7 | assert(typeof(t[k]) == "table", "'" .. keyName .. "' is not of type 'table'") 8 | assertTypes(keyName, t[k], v) 9 | else 10 | assert(typeof(t[k]) == v, "'" .. keyName .. "' is not of type '" .. v .. "'") 11 | end 12 | end 13 | end 14 | 15 | local function printChildren(obj, depth) 16 | depth = depth and depth + 1 or 0 17 | print(string.rep(" ", depth) .. obj.Name) 18 | for _, v in ipairs(obj:GetChildren()) do 19 | printChildren(v, depth) 20 | end 21 | end 22 | 23 | warn("Assert package types") 24 | do 25 | local package 26 | package = Rostruct.fetchAsync("Roblox", "roact", "v1.4.0") 27 | package = Rostruct.fetchLatestAsync("Roblox", "roact") 28 | assertTypes("(Roact) Package", package, { 29 | tree = "Instance", 30 | root = "string", 31 | fetchInfo = { 32 | location = "string", 33 | owner = "string", 34 | repo = "string", 35 | tag = "string", 36 | asset = "string", 37 | updated = "boolean", 38 | }, 39 | }) 40 | end 41 | 42 | warn("Test MidiPlayer") 43 | do 44 | local package = Rostruct.open("MidiPlayer/") 45 | assertTypes("(MidiPlayer) Package", package, { 46 | tree = "Instance", 47 | root = "string", 48 | fetchInfo = "nil", 49 | }) 50 | 51 | -- Deploy project 52 | package:build("src/") 53 | package:start() 54 | printChildren(package.tree) 55 | 56 | -- Require inner module 57 | assert(type(package:requireAsync(package.tree.MidiPlayer.MIDI)) == "table", "Failed to require Tree.MidiPlayer.MIDI") 58 | 59 | -- Require specific file 60 | package:build("src/Util/Thread.lua") 61 | assert(type(package:requireAsync(package.tree.Thread)) == "table", "Failed to require Tree.Thread") 62 | end 63 | 64 | warn("Test all file types") 65 | do 66 | local package = Rostruct.open("tests/build/") 67 | package:build() 68 | package:start():await() 69 | end 70 | 71 | warn("Require Roact") 72 | do 73 | local package = Rostruct.fetchAsync("Roblox", "roact", "v1.4.0") 74 | 75 | local Roact = package:requireAsync( 76 | package:build("src/", { Name = "Roact" }) 77 | ) 78 | 79 | assert( 80 | type(Roact) == "table" and type(Roact.createElement) == "function", 81 | "Failed to require Roact" 82 | ) 83 | end 84 | 85 | warn("Require Roact inline") 86 | do 87 | local Roact = Rostruct.fetchLatest("Roblox", "roact") 88 | :andThen(function(package) 89 | return package:require(package:build("src/", { Name = "Roact" })) 90 | end) 91 | :expect() 92 | 93 | assert( 94 | type(Roact) == "table" and type(Roact.createElement) == "function", 95 | "Failed to require Roact inline with tree" 96 | ) 97 | end 98 | 99 | warn("Require Roact inline with Roact.rbxm") 100 | do 101 | local Roact = Rostruct.fetchLatest("Roblox", "roact", "Roact.rbxm") 102 | :andThen(function(package) 103 | package:build("Roact.rbxm") 104 | return package:require(package.tree.Roact) 105 | end) 106 | :expect() 107 | 108 | assert( 109 | type(Roact) == "table" and type(Roact.createElement) == "function", 110 | "Failed to require Roact inline with tree" 111 | ) 112 | end 113 | 114 | print("Ok!") 115 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build your Lua projects from the filesystem. 3 | * @author 0866 4 | */ 5 | 6 | import { bootstrap } from "bootstrap"; 7 | 8 | bootstrap(); 9 | 10 | import { Package } from "Package"; 11 | import { clearReleaseCache, downloadLatestRelease, downloadRelease } from "utils/fetch-github-release"; 12 | 13 | /** Clears the GitHub Release cache. */ 14 | export const clearCache = () => clearReleaseCache(); 15 | 16 | /** 17 | * Creates a new Rostruct Package. 18 | * @param root A path to the project directory. 19 | * @returns A new Package object. 20 | */ 21 | export const open = (root: string): Package => new Package(root); 22 | 23 | /** 24 | * Downloads and builds a release from the given repository. 25 | * If `asset` is undefined, it downloads source files through the zipball URL. 26 | * Automatically extracts .zip files. 27 | * 28 | * @param owner The owner of the repository. 29 | * @param repo The name of the repository. 30 | * @param tag The tag version to download. 31 | * @param asset Optional asset to download; If not specified, it downloads the source files. 32 | * 33 | * @returns A promise that resolves with a Package object, with the `fetchInfo` field. 34 | */ 35 | export const fetch = async (...args: Parameters): Promise => 36 | Package.fromFetch(await downloadRelease(...args)); 37 | 38 | /** 39 | * Downloads and builds a release from the given repository. 40 | * If `asset` is undefined, it downloads source files through the zipball URL. 41 | * Automatically extracts .zip files. 42 | * 43 | * @param owner The owner of the repository. 44 | * @param repo The name of the repository. 45 | * @param tag The tag version to download. 46 | * @param asset Optional asset to download; If not specified, it downloads the source files. 47 | * 48 | * @returns A new Package object, with the `fetchInfo` field. 49 | */ 50 | export const fetchAsync = (...args: Parameters): Package => 51 | Package.fromFetch(downloadRelease(...args).expect()); 52 | 53 | /** 54 | * **This function does not download prereleases or drafts.** 55 | * 56 | * Downloads and builds the latest release release from the given repository. 57 | * If `asset` is undefined, it downloads source files through the zipball URL. 58 | * Automatically extracts .zip files. 59 | * 60 | * @param owner The owner of the repository. 61 | * @param repo The name of the repository. 62 | * @param asset Optional asset to download; If not specified, it downloads the source files. 63 | * 64 | * @returns A promise that resolves with a Package object, with the `fetchInfo` field. 65 | */ 66 | export const fetchLatest = async (...args: Parameters): Promise => 67 | Package.fromFetch(await downloadLatestRelease(...args)); 68 | 69 | /** 70 | * **This function does not download prereleases or drafts.** 71 | * 72 | * Downloads and builds the latest release release from the given repository. 73 | * If `asset` is undefined, it downloads source files through the zipball URL. 74 | * Automatically extracts .zip files. 75 | * 76 | * @param owner The owner of the repository. 77 | * @param repo The name of the repository. 78 | * @param asset Optional asset to download; If not specified, it downloads the source files. 79 | * 80 | * @returns A new Package object, with the `fetchInfo` field. 81 | */ 82 | export const fetchLatestAsync = (...args: Parameters): Package => 83 | Package.fromFetch(downloadLatestRelease(...args).expect()); 84 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project 2 | site_name: Rostruct 3 | site_url: https://richie0866.github.io/Rostruct/ 4 | site_description: >- 5 | A modern exploiting solution, built for Roblox and Rojo. 6 | Deploy your project files to a Roblox script executor with a simple and flexible execution library. 7 | 8 | # Repository 9 | repo_name: richie0866/Rostruct 10 | repo_url: https://github.com/richie0866/Rostruct 11 | 12 | # Configuration 13 | theme: 14 | name: material 15 | custom_dir: docs/assets/overrides 16 | 17 | font: 18 | code: JetBrains Mono 19 | text: Inter 20 | 21 | highlightjs: true 22 | 23 | hljs_languages: 24 | - typescript 25 | - lua 26 | 27 | palette: 28 | scheme: slate 29 | primary: 'blue' 30 | accent: 'Light blue' 31 | 32 | features: 33 | - navigation.tabs 34 | - navigation.sections 35 | 36 | # Extra 37 | extra_css: 38 | - assets/stylesheets/extra.css 39 | - assets/stylesheets/api-tags.css 40 | 41 | # Extensions 42 | markdown_extensions: 43 | - admonition 44 | - abbr 45 | - attr_list 46 | - def_list 47 | - footnotes 48 | - meta 49 | - md_in_html 50 | - toc: 51 | permalink: true 52 | - pymdownx.arithmatex: 53 | generic: true 54 | - pymdownx.betterem: 55 | smart_enable: all 56 | - pymdownx.caret 57 | - pymdownx.critic 58 | - pymdownx.details 59 | - pymdownx.emoji: 60 | emoji_index: !!python/name:materialx.emoji.twemoji 61 | emoji_generator: !!python/name:materialx.emoji.to_svg 62 | - pymdownx.highlight 63 | - pymdownx.inlinehilite 64 | - pymdownx.keys 65 | - pymdownx.magiclink: 66 | repo_url_shorthand: true 67 | user: squidfunk 68 | repo: mkdocs-material 69 | - pymdownx.mark 70 | - pymdownx.smartsymbols 71 | - pymdownx.superfences: 72 | custom_fences: 73 | - name: mermaid 74 | class: mermaid 75 | format: !!python/name:pymdownx.superfences.fence_code_format 76 | - pymdownx.tabbed 77 | - pymdownx.tasklist: 78 | custom_checkbox: true 79 | - pymdownx.tilde 80 | 81 | # Plugins 82 | plugins: 83 | - macros 84 | - search 85 | 86 | # Page tree 87 | nav: 88 | - Home: index.md 89 | 90 | - Getting started: 91 | - Overview: getting-started/overview.md 92 | - Installation: getting-started/installation.md 93 | - Usage: 94 | - Creating your project: getting-started/creating-your-project.md 95 | - Publishing your project: getting-started/publishing-your-project.md 96 | - Using other projects: getting-started/using-other-projects.md 97 | - Execution model: getting-started/execution-model.md 98 | 99 | - API Reference: 100 | - Overview: api-reference/overview.md 101 | - Types: api-reference/types.md 102 | - Rostruct: 103 | - Functions: 104 | - open: api-reference/rostruct/open.md 105 | - fetch: api-reference/rostruct/fetch.md 106 | - fetchLatest: api-reference/rostruct/fetchlatest.md 107 | - clearCache: api-reference/rostruct/clearcache.md 108 | - Package: 109 | - Properties: api-reference/package/properties.md 110 | - Methods: 111 | - build: api-reference/package/build.md 112 | - start: api-reference/package/start.md 113 | - require: api-reference/package/require.md 114 | - File conversion: api-reference/file-conversion.md 115 | - Globals: api-reference/globals.md 116 | 117 | - Featured: 118 | - Community: featured/community.md 119 | -------------------------------------------------------------------------------- /src/core/build/index.ts: -------------------------------------------------------------------------------- 1 | import { Session } from "core/Session"; 2 | import { pathUtils } from "utils/file-utils"; 3 | import { makeLocalizationTable } from "./csv"; 4 | import { makeDir } from "./dir"; 5 | import { makeJsonModule } from "./json"; 6 | import { makeJsonModel } from "./json-model"; 7 | import { makeLua, makeLuaInit } from "./lua"; 8 | import { makeRobloxModel } from "./rbx-model"; 9 | import { makePlainText } from "./txt"; 10 | 11 | /** 12 | * Tries to turn the file or directory at `path` into an Instance. This function is recursive! 13 | * @param session The current Session. 14 | * @param path The file to turn into an object. 15 | * @returns The Instance made from the file. 16 | */ 17 | export function build(session: Session, path: string): Instance | undefined { 18 | if (isfolder(path)) { 19 | let instance: Instance; 20 | 21 | const luaInitPath = pathUtils.locateFiles(path, ["init.lua", "init.server.lua", "init.client.lua"]); 22 | 23 | if (luaInitPath !== undefined) { 24 | instance = makeLuaInit(session, path + luaInitPath); 25 | } else { 26 | instance = makeDir(path); 27 | } 28 | 29 | // Populate the instance here! This is a workaround for a possible 30 | // cyclic reference when attempting to call 'makeObject' from another 31 | // file. 32 | for (const child of listfiles(pathUtils.trimTrailingSlash(path))) { 33 | const childInstance = build(session, pathUtils.addTrailingSlash(child)); 34 | if (childInstance) childInstance.Parent = instance; 35 | } 36 | 37 | return instance; 38 | } else if (isfile(path)) { 39 | const name = pathUtils.getName(path); 40 | 41 | // Lua script 42 | // https://rojo.space/docs/6.x/sync-details/#scripts 43 | if ( 44 | name.match("(%.lua)$")[0] !== undefined && 45 | name.match("^(init%.)")[0] === undefined // Ignore init scripts 46 | ) { 47 | return makeLua(session, path); 48 | } 49 | // Ignore meta files 50 | else if (name.match("(%.meta.json)$")[0] !== undefined) return; 51 | // JSON model 52 | // https://rojo.space/docs/6.x/sync-details/#json-models 53 | else if (name.match("(%.model.json)$")[0] !== undefined) { 54 | return makeJsonModel(path, name.match("^(.*)%.model.json$")[0] as string); 55 | } 56 | // Project node 57 | // Unsupported by Rostruct 58 | else if (name.match("(%.project.json)$")[0] !== undefined) { 59 | warn(`Project files are not supported (${path})`); 60 | } 61 | // JSON module 62 | // https://rojo.space/docs/6.x/sync-details/#json-modules 63 | else if (name.match("(%.json)$")[0] !== undefined) { 64 | return makeJsonModule(session, path, name.match("^(.*)%.json$")[0] as string); 65 | } 66 | // Localization table 67 | // https://rojo.space/docs/6.x/sync-details/#localization-tables 68 | else if (name.match("(%.csv)$")[0] !== undefined) { 69 | return makeLocalizationTable(path, name.match("^(.*)%.csv$")[0] as string); 70 | } 71 | // Plain text 72 | // https://rojo.space/docs/6.x/sync-details/#plain-text 73 | else if (name.match("(%.txt)$")[0] !== undefined) { 74 | return makePlainText(path, name.match("^(.*)%.txt$")[0] as string); 75 | } 76 | // Binary model 77 | // https://rojo.space/docs/6.x/sync-details/#models 78 | else if (name.match("(%.rbxm)$")[0] !== undefined) { 79 | return makeRobloxModel(session, path, name.match("^(.*)%.rbxm$")[0] as string); 80 | } 81 | // XML model 82 | // https://rojo.space/docs/6.x/sync-details/#models 83 | else if (name.match("(%.rbxmx)$")[0] !== undefined) { 84 | return makeRobloxModel(session, path, name.match("^(.*)%.rbxmx$")[0] as string); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/core/build/csv.ts: -------------------------------------------------------------------------------- 1 | import Make from "modules/make"; 2 | import { pathUtils } from "utils/file-utils"; 3 | import { fileMetadata } from "./metadata"; 4 | 5 | type SettableEntryPropertyName = Exclude; 6 | 7 | const settableEntryPropertyNames: SettableEntryPropertyName[] = ["Context", "Example", "Key", "Source"]; 8 | 9 | /** Reads a CSV file and turns it into an array of `LocalizationEntries`. */ 10 | class CsvReader { 11 | /** A list of entries that can be passed to `LocalizationTable.SetEntries()`. */ 12 | public entries: LocalizationEntry[] = []; 13 | 14 | /** The header of the CSV file. Used to map entry columns to the column name. */ 15 | private keys: SettableEntryPropertyName[] = []; 16 | 17 | constructor( 18 | /** Raw file data. */ 19 | public readonly raw: string, 20 | 21 | /** The raw data split by row. */ 22 | public readonly buffer = raw.split("\n"), 23 | ) {} 24 | 25 | /** 26 | * Reads the CSV file and turns it into an array of `LocalizationEntries`. 27 | * @returns A list of localization entries. 28 | */ 29 | public read() { 30 | // (i === 1) since otherwise transpiled to (i == 0) 31 | for (const [i, line] of ipairs(this.buffer)) 32 | if (i === 1) this.readHeader(line); 33 | else this.readEntry(line); 34 | 35 | return this.entries; 36 | } 37 | 38 | /** 39 | * Turns the header into an array of keys to be used as entry properties. 40 | * @param currentLine The first line of the CSV file. 41 | */ 42 | public readHeader(currentLine: string) { 43 | this.keys = currentLine.split(",") as SettableEntryPropertyName[]; 44 | } 45 | 46 | /** 47 | * Checks if an entry can have the type of `LocalizationEntry`. 48 | * @param entry 49 | */ 50 | public validateEntry(entry: LocalizationEntry): boolean { 51 | return ( 52 | entry.Context !== undefined && 53 | entry.Key !== undefined && 54 | entry.Source !== undefined && 55 | entry.Values !== undefined 56 | ); 57 | } 58 | 59 | /** 60 | * Creates a `LocalizationEntry` for the line in the CSV file. 61 | * @param currentLine A line from the CSV file. 62 | */ 63 | public readEntry(currentLine: string) { 64 | const entry: Partial & { Values: LocalizationEntry["Values"] } = { 65 | Values: new Map(), 66 | }; 67 | 68 | // (i - 1) since otherwise transpiled to (i + 1) 69 | for (const [i, value] of ipairs(currentLine.split(","))) { 70 | const key = this.keys[i - 1]; 71 | 72 | // If 'key' is a property of the entry, then set it to value. 73 | // Otherwise, add it to the 'Values' map for locale ids. 74 | if (settableEntryPropertyNames.includes(key)) entry[key] = value; 75 | else entry.Values.set(key, value); 76 | } 77 | 78 | if (this.validateEntry(entry as LocalizationEntry)) this.entries.push(entry as LocalizationEntry); 79 | } 80 | } 81 | 82 | /** 83 | * Transforms a CSV file into a Roblox LocalizationTable. 84 | * @param path A path to the CSV file. 85 | * @param name The name of the instance. 86 | * @returns A LocalizationTable with entries configured. 87 | */ 88 | export function makeLocalizationTable(path: string, name: string) { 89 | const csvReader = new CsvReader(readfile(path)); 90 | 91 | const locTable = Make("LocalizationTable", { Name: name }); 92 | locTable.SetEntries(csvReader.read()); 93 | 94 | // Applies an adjacent meta file if it exists. 95 | const metaPath = pathUtils.getParent(path) + name + ".meta.json"; 96 | if (isfile(metaPath)) fileMetadata(metaPath, locTable); 97 | 98 | return locTable; 99 | } 100 | -------------------------------------------------------------------------------- /src/modules/object-utils/index.d.ts: -------------------------------------------------------------------------------- 1 | interface ObjectConstructor { 2 | /** 3 | * Copy the values of all of the enumerable own properties from one or more source objects to a target object. 4 | * Returns the target object. 5 | */ 6 | assign(this: void, target: A, source: B): A & B; 7 | assign(this: void, target: A, source1: B, source2: C): A & B & C; 8 | assign(this: void, target: A, source1: B, source2: C, source3: D): A & B & C & D; 9 | assign(this: void, target: A, source1: B, source2: C, source3: D, source4: E): A & B & C & D & E; 10 | assign( 11 | this: void, 12 | target: A, 13 | source1: B, 14 | source2: C, 15 | source3: D, 16 | source4: E, 17 | source5: F, 18 | ): A & B & C & D & E & F; 19 | assign(this: void, target: object, ...sources: Array): any; 20 | 21 | /** 22 | * Returns the names of the enumerable properties and methods of an object. 23 | * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object. 24 | */ 25 | keys(this: void, o: ReadonlyArray): Array; 26 | keys(this: void, o: ReadonlySet): Array; 27 | keys(this: void, o: ReadonlyMap): Array; 28 | keys(this: void, o: T): keyof T extends never ? Array : Array; 29 | 30 | /** 31 | * Returns an array of values of the enumerable properties of an object 32 | * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object. 33 | */ 34 | values(this: void, o: ReadonlyArray): Array>; 35 | values(this: void, o: ReadonlySet): Array; 36 | values(this: void, o: ReadonlyMap): Array>; 37 | values(this: void, o: T): keyof T extends never ? Array : Array>; 38 | 39 | /** 40 | * Returns an array of key/values of the enumerable properties of an object 41 | * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object. 42 | */ 43 | entries(this: void, o: ReadonlyArray): Array<[number, NonNullable]>; 44 | entries(this: void, o: ReadonlySet): Array<[T, true]>; 45 | entries(this: void, o: ReadonlyMap): Array<[K, NonNullable]>; 46 | entries( 47 | this: void, 48 | o: T, 49 | ): keyof T extends never ? Array<[unknown, unknown]> : Array<[keyof T, NonNullable]>; 50 | 51 | /** Creates an object from a set of entries */ 52 | fromEntries

( 53 | this: void, 54 | i: ReadonlyArray

, 55 | ): Reconstruct< 56 | UnionToIntersection< 57 | P extends unknown 58 | ? { 59 | [k in P[0]]: P[1]; 60 | } 61 | : never 62 | > 63 | >; 64 | 65 | /** 66 | * Returns true if empty, otherwise false. 67 | */ 68 | isEmpty(this: void, o: object): boolean; 69 | 70 | /** 71 | * Returns a shallow copy of the object 72 | */ 73 | copy(this: void, o: T): T; 74 | 75 | /** 76 | * Returns a deep copy of the object 77 | */ 78 | deepCopy(this: void, o: T): T; 79 | 80 | /** 81 | * Returns true if 82 | * - each member of `a` equals each member of `b` 83 | * - `b` has no members that do not exist in `a`. 84 | * 85 | * Searches recursively. 86 | */ 87 | deepEquals(this: void, a: object, b: object): boolean; 88 | } 89 | 90 | declare const Object: ObjectConstructor; 91 | 92 | export = Object; 93 | -------------------------------------------------------------------------------- /src/utils/fetch-github-release/types.ts: -------------------------------------------------------------------------------- 1 | /** Information about the release being downloaded. */ 2 | export interface FetchInfo { 3 | /** The folder the release was saved to. */ 4 | readonly location: string; 5 | 6 | /** The owner of the repository. */ 7 | readonly owner: string; 8 | 9 | /** The name of the repository. */ 10 | readonly repo: string; 11 | 12 | /** The version of the release. */ 13 | readonly tag: string; 14 | 15 | /** The specific asset that was downloaded. */ 16 | readonly asset: "Source code" | string; 17 | 18 | /** Whether the operation downloaded a new release. */ 19 | readonly updated: boolean; 20 | } 21 | 22 | export interface Author { 23 | readonly login: string; 24 | readonly id: number; 25 | readonly node_id: string; 26 | /** @example `https://avatars.githubusercontent.com/u/${id}?v=4` */ 27 | readonly avatar_url: string; 28 | readonly gravatar_id: string; 29 | /** @example `https://api.github.com/users/${user}` */ 30 | readonly url: string; 31 | /** @example `https://github.com/${user}` */ 32 | readonly html_url: string; 33 | /** @example `https://api.github.com/users/${user}/followers` */ 34 | readonly followers_url: string; 35 | /** @example `https://api.github.com/users/${user}/following{/other_user}` */ 36 | readonly following_url: string; 37 | /** @example `https://api.github.com/users/${user}/gists{/gist_id}` */ 38 | readonly gists_url: string; 39 | /** @example `https://api.github.com/users/${user}/starred{/owner}{/repo}` */ 40 | readonly starred_url: string; 41 | /** @example `https://api.github.com/users/${user}/subscriptions` */ 42 | readonly subscriptions_url: string; 43 | /** @example `https://api.github.com/users/${user}/orgs` */ 44 | readonly organizations_url: string; 45 | /** @example `https://api.github.com/users/${user}/repos` */ 46 | readonly repos_url: string; 47 | /** @example `https://api.github.com/users/${user}/events{/privacy}` */ 48 | readonly events_url: string; 49 | /** @example `https://api.github.com/users/${user}/received_events` */ 50 | readonly received_events_url: string; 51 | readonly type: string; 52 | readonly site_admin: boolean; 53 | } 54 | 55 | export interface Asset { 56 | /** @example `https://api.github.com/repos/${user}/${repo}/releases/assets/${id}` */ 57 | readonly url: string; 58 | readonly id: number; 59 | readonly node_id: string; 60 | readonly name: string; 61 | readonly label?: string; 62 | readonly uploader: Author; 63 | readonly content_type: string; 64 | readonly state: string; 65 | readonly size: number; 66 | readonly download_count: number; 67 | readonly created_at: string; 68 | readonly updated_at: string; 69 | /** @example `https://github.com/${user}/${repo}/releases/download/${tag}/%{asset}` */ 70 | readonly browser_download_url: string; 71 | } 72 | 73 | /** 74 | * Information about the latest release of a given Github repository. 75 | * See this [example](https://api.github.com/repos/Roblox/roact/releases/latest). 76 | */ 77 | export interface Release { 78 | /** @example `https://api.github.com/repos/${user}/${repo}/releases/${id}` */ 79 | readonly url: string; 80 | /** @example `https://api.github.com/repos/${user}/${repo}/releases/${id}/assets` */ 81 | readonly assets_url: string; 82 | /** @example `https://uploads.github.com/repos/${user}/${repo}/releases/${id}/assets{?name,label}` */ 83 | readonly upload_url: string; 84 | /** @example `https://github.com/${user}/${repo}/releases/tag/${latestVersion}` */ 85 | readonly html_url: string; 86 | readonly id: number; 87 | readonly author: Author; 88 | readonly node_id: string; 89 | readonly tag_name: string; 90 | readonly target_commitish: string; 91 | readonly name: string; 92 | readonly draft: boolean; 93 | readonly prerelease: boolean; 94 | readonly created_at: string; 95 | readonly published_at: string; 96 | readonly assets: Asset[]; 97 | /** @example `https://api.github.com/repos/${user}/${repo}/tarball/${tag}` */ 98 | readonly tarball_url: string; 99 | /** @example `https://api.github.com/repos/${user}/${repo}/zipball/${tag}` */ 100 | readonly zipball_url: string; 101 | readonly body: string; 102 | } 103 | -------------------------------------------------------------------------------- /src/Package.ts: -------------------------------------------------------------------------------- 1 | import { Session, VirtualScript } from "core"; 2 | import type { Executor } from "core"; 3 | import type { FetchInfo } from "utils/fetch-github-release"; 4 | import { pathUtils } from "utils/file-utils"; 5 | import Make from "modules/make"; 6 | 7 | /** Transforms files into Roblox objects and handles runtime. Acts as a wrapper for Session. */ 8 | export class Package { 9 | /** A Folder containing objects created from the `build()` method. */ 10 | public readonly tree = Make("Folder", { Name: "Tree" }); 11 | 12 | /** The root directory of the project. */ 13 | public readonly root: string; 14 | 15 | /** Information about the last call of `fetch` or `fetchLatest`. */ 16 | public readonly fetchInfo?: FetchInfo; 17 | 18 | /** The Session for this project. */ 19 | private readonly session: Session; 20 | 21 | /** 22 | * Create a new Rostruct Package. 23 | * @param root A path to the project directory. 24 | * @param fetchInfo Information about the downloaded GitHub release. 25 | */ 26 | constructor(root: string, fetchInfo?: FetchInfo) { 27 | assert(type(root) === "string", "(Package) The path must be a string"); 28 | assert(isfolder(root), `(Package) The path '${root}' must be a valid directory`); 29 | this.root = pathUtils.formatPath(root); 30 | this.session = new Session(root); 31 | this.fetchInfo = fetchInfo; 32 | } 33 | 34 | /** 35 | * Create a new Package from a downloaded GitHub release. 36 | * @param root A path to the project directory. 37 | * @param fetchInfo Information about the downloaded GitHub release. 38 | */ 39 | public static readonly fromFetch = (fetchInfo: FetchInfo): Package => new Package(fetchInfo.location, fetchInfo); 40 | 41 | /** 42 | * Turns a folder in the root directory and all descendants into Roblox objects. 43 | * If `dir` is not provided, this function transforms the root directory. 44 | * @param fileOrFolder Optional specific folder to build in the root directory. 45 | * @param props Optional properties to set after building the folder. 46 | * @returns The Instance created. 47 | */ 48 | public build(fileOrFolder = "", props?: { [property: string]: unknown }): Instance { 49 | assert( 50 | isfile(this.root + fileOrFolder) || isfolder(this.root + fileOrFolder), 51 | `(Package.build) The path '${this.root + fileOrFolder}' must be a file or folder`, 52 | ); 53 | 54 | const instance = this.session.build(fileOrFolder); 55 | assert(instance, `(Package.build) The path '${this.root + fileOrFolder}' could not be turned into an Instance`); 56 | 57 | // Set object properties 58 | if (props !== undefined) { 59 | for (const [property, value] of pairs(props as unknown as Map)) { 60 | instance[property] = value; 61 | } 62 | } 63 | 64 | instance.Parent = this.tree; 65 | 66 | return instance; 67 | } 68 | 69 | /** 70 | * Simulate script runtime by running LocalScripts on deferred threads. 71 | * @returns 72 | * A promise that resolves with an array of scripts that finished executing. 73 | * If one script throws an error, the entire promise will cancel. 74 | */ 75 | public start(): Promise { 76 | return this.session.simulate(); 77 | } 78 | 79 | /** 80 | * Requires the given ModuleScript. If `module` is not provided, it requires 81 | * the first Instance in `tree`. 82 | * @param module The module to require. 83 | * @returns A promise that resolves with what the module returned. 84 | */ 85 | public async require(module: ModuleScript): Promise> { 86 | assert(classIs(module, "ModuleScript"), `(Package.require) '${module}' must be a module`); 87 | assert(module.IsDescendantOf(this.tree), `(Package.require) '${module}' must be a descendant of Package.tree`); 88 | return VirtualScript.requireFromInstance(module); 89 | } 90 | 91 | /** 92 | * Requires the given ModuleScript. If `module` is not provided, it requires 93 | * the first Instance in `tree`. 94 | * @param module The module to require. 95 | * @returns What the module returned. 96 | */ 97 | public requireAsync(module: ModuleScript): ReturnType { 98 | return this.require(module).expect(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /typings/Engine.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the Roblox global environment. 3 | * @returns Roblox's global environment. 4 | */ 5 | declare function getrenv(): { [key: string]: unknown }; 6 | 7 | /** 8 | * Returns the current environment in use by the exploit. 9 | * @returns The exploit's global environment. 10 | * */ 11 | declare function getgenv(): { [key: string]: unknown }; 12 | 13 | /** 14 | * Loads a chunk. 15 | * If there are no syntactic errors, returns the compiled chunk as a function; otherwise, returns nil plus the error message. 16 | * 17 | * `chunkname` is used as the name of the chunk for error messages and debug information. 18 | * When absent, it defaults to chunk, if chunk is a string, or to "=(load)" otherwise. 19 | * @param chunk The string to load. 20 | * @param chunkname The name of the chunk. 21 | */ 22 | declare function loadstring( 23 | chunk: string, 24 | chunkname?: string, 25 | ): LuaTuple<[(...params: Array) => unknown, string | undefined]>; 26 | 27 | /** Returns the name of the executor. */ 28 | declare function identifyexecutor(): string; 29 | 30 | // Filesystem 31 | 32 | /** Check whether the given path points to a file. */ 33 | declare function isfile(path: string): boolean; 34 | 35 | /** Check whether the given path points to a directory. */ 36 | declare function isfolder(path: string): boolean; 37 | 38 | /** Load the given file and return its contents. */ 39 | declare function readfile(file: string): string; 40 | 41 | /** Change the given file's contents. Creates a new file if it doesn't exist. */ 42 | declare function writefile(file: string, content: string): void; 43 | 44 | /** Returns a list of file paths in the given directory. */ 45 | declare function listfiles(directory: string): Array; 46 | 47 | /** Create a new directory at the given path. */ 48 | declare function makefolder(directory: string): void; 49 | 50 | /** Removes the directory at the given path. */ 51 | declare function delfolder(directory: string): void; 52 | 53 | /** Generates a rbxasset:// [`content`](https://developer.roblox.com/en-us/articles/Content) URL for the given asset from Krnl's `workspace` directory. */ 54 | declare function getcustomasset(file: string): string; 55 | 56 | /** Sends an HTTP request using a dictionary to specify the request data, such as the target URL, method, headers and request body data. It returns a dictionary that describes the response data received. */ 57 | declare function request(requestOptions: RequestAsyncRequest): RequestAsyncResponse; 58 | 59 | /** Encodes the text to Base64. */ 60 | declare function base64_encode(text: string): string; 61 | 62 | /** Encodes the text from Base64. */ 63 | declare function base64_decode(base64: string): string; 64 | 65 | /** Hashes the text using the [SHA384](https://en.wikipedia.org/wiki/SHA-2) cipher. */ 66 | declare function sha384_hash(text: string): string; 67 | 68 | interface DataModel { 69 | /** Sends an HTTP GET request. */ 70 | HttpGetAsync(this: DataModel, url: string): string; 71 | 72 | /** Sends an HTTP POST request. */ 73 | HttpPostAsync(this: DataModel, url: string): string; // TODO: Check what it actually returns 74 | 75 | /** Returns an array of Instances associated with the given [`content`](https://developer.roblox.com/en-us/articles/Content) URL. */ 76 | GetObjects(this: DataModel, url: string): Instance[]; 77 | } 78 | 79 | // Synapse 80 | 81 | declare const syn: { 82 | request: typeof request; 83 | }; 84 | 85 | declare const getsynasset: typeof getcustomasset; 86 | 87 | // Scriptware 88 | 89 | declare const http: { 90 | request: typeof request; 91 | }; 92 | 93 | // Roblox 94 | 95 | /** 96 | * Sets the environment to be used by the given function. `f` can be a Lua function or a number that specifies the function at that stack level: 97 | * Level 1 is the function calling `setfenv`. `setfenv` returns the given function. 98 | * 99 | * As a special case, when f is 0 setfenv changes the environment of the running thread. In this case, setfenv returns no values. 100 | * 101 | * @param f A Lua function or the stack level. 102 | * @param env The new environment. 103 | */ 104 | declare function setfenv(f: T, env: object): T extends 0 ? undefined : T; 105 | -------------------------------------------------------------------------------- /src/utils/fetch-github-release/downloadRelease.ts: -------------------------------------------------------------------------------- 1 | import { JsonStore } from "utils/JsonStore"; 2 | import { downloadAsset } from "./downloadAsset"; 3 | import { bootstrap, getRostructPath } from "bootstrap"; 4 | import { identify } from "./identify"; 5 | import { getLatestRelease, getRelease } from "./getReleases"; 6 | import type { FetchInfo } from "./types"; 7 | 8 | /** Object used to modify the JSON file with decoded JSON data. */ 9 | const savedTags = new JsonStore(getRostructPath("RELEASE_TAGS")); 10 | 11 | /** 12 | * Downloads a release from the given repository. If `asset` is undefined, it downloads 13 | * the source zip files and extracts them. Automatically extracts .zip files. 14 | * This function does not download prereleases or drafts. 15 | * @param owner The owner of the repository. 16 | * @param repo The name of the repository. 17 | * @param tag The release tag to download. 18 | * @param asset Optional asset to download. Defaults to the source files. 19 | * @returns A download result interface. 20 | */ 21 | export async function downloadRelease(owner: string, repo: string, tag: string, asset?: string): Promise { 22 | // Type assertions: 23 | assert(type(owner) === "string", "Argument 'owner' must be a string"); 24 | assert(type(repo) === "string", "Argument 'repo' must be a string"); 25 | assert(type(tag) === "string", "Argument 'tag' must be a string"); 26 | assert(asset === undefined || type(asset) === "string", "Argument 'asset' must be a string or nil"); 27 | 28 | const id = identify(owner, repo, tag, asset); 29 | const path = getRostructPath("RELEASE_CACHE") + id + "/"; 30 | 31 | // If the path is taken, don't download it again 32 | if (isfolder(path)) 33 | return { 34 | location: path, 35 | owner: owner, 36 | repo: repo, 37 | tag: tag, 38 | asset: asset ?? "Source code", 39 | updated: false, 40 | }; 41 | 42 | const release = await getRelease(owner, repo, tag); 43 | await downloadAsset(release, path, asset); 44 | 45 | return { 46 | location: path, 47 | owner: owner, 48 | repo: repo, 49 | tag: tag, 50 | asset: asset ?? "Source code", 51 | updated: true, 52 | }; 53 | } 54 | 55 | /** 56 | * Downloads the latest release from the given repository. If `asset` is undefined, 57 | * it downloads the source zip files and extracts them. Automatically extracts .zip files. 58 | * This function does not download prereleases or drafts. 59 | * @param owner The owner of the repository. 60 | * @param repo The name of the repository. 61 | * @param asset Optional asset to download. Defaults to the source files. 62 | * @returns A download result interface. 63 | */ 64 | export async function downloadLatestRelease(owner: string, repo: string, asset?: string): Promise { 65 | // Type assertions: 66 | assert(type(owner) === "string", "Argument 'owner' must be a string"); 67 | assert(type(repo) === "string", "Argument 'repo' must be a string"); 68 | assert(asset === undefined || type(asset) === "string", "Argument 'asset' must be a string or nil"); 69 | 70 | const id = identify(owner, repo, undefined, asset); 71 | const path = getRostructPath("RELEASE_CACHE") + id + "/"; 72 | 73 | const release = await getLatestRelease(owner, repo); 74 | 75 | savedTags.open(); 76 | 77 | // Check if the cache is up-to-date 78 | if (savedTags.get(id) === release.tag_name && isfolder(path)) { 79 | savedTags.close(); 80 | return { 81 | location: path, 82 | owner: owner, 83 | repo: repo, 84 | tag: release.tag_name, 85 | asset: asset ?? "Source code", 86 | updated: false, 87 | }; 88 | } 89 | 90 | // Update the cache with the new tag 91 | savedTags.set(id, release.tag_name); 92 | savedTags.close(); 93 | 94 | // Make sure nothing is at the path before downloading! 95 | if (isfolder(path)) delfolder(path); 96 | 97 | // Download the asset to the cache 98 | await downloadAsset(release, path, asset); 99 | 100 | return { 101 | location: path, 102 | owner: owner, 103 | repo: repo, 104 | tag: release.tag_name, 105 | asset: asset ?? "Source code", 106 | updated: true, 107 | }; 108 | } 109 | 110 | /** Clears the release cache. */ 111 | export function clearReleaseCache() { 112 | delfolder(getRostructPath("RELEASE_CACHE")); 113 | bootstrap(); 114 | } 115 | -------------------------------------------------------------------------------- /docs/assets/images/vs-code-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/getting-started/overview.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | With **Rostruct**, script executors can run your Lua projects as Roblox Instances. Integrate powerful tools like [Rojo](https://rojo.space/docs/) and [Selene for VS Code](https://marketplace.visualstudio.com/items?itemName=Kampfkarren.selene-vscode) into your workflow, ensuring a hassle-free development experience. 4 | 5 | This documentation is a work in progress! 6 | 7 | ## What you need to know 8 | 9 | This guide assumes: 10 | 11 | - [x] You're familiar with development in an external code editor. 12 | * If you're unsure of how to manage Roblox projects externally, or what we're trying to accomplish, check out the [Rojo docs](https://rojo.space/) for a detailed explanation. 13 | 14 | - [x] You understand how to use Promises. 15 | * Though the Promise-based API is optional, it's very useful. If you'd like to learn more, check out evaera's [Promise library](https://eryn.io/roblox-lua-promise/). 16 | 17 | ## Why Rostruct? 18 | 19 | When it comes to exploiting, projects are often developed and maintained within a single file. However, scripts that get too large become detrimental to your workflow. Over time, your project becomes more difficult to debug, maintain, and share. 20 | 21 | In contrast, with Rojo, your codebase gets turned directly into Roblox Instances. Taking this modular approach to exploiting can significantly improve the development experience. 22 | 23 | Rostruct's design complements a Rojo-based workflow, introducing script developers to a professional way to manage projects. 24 | 25 | --- 26 | 27 | ![Script hub example](../assets/images/script-hub-panel.svg){ align=right width=200 draggable=false } 28 | 29 | ### Built for ambitious projects 30 | 31 | Rostruct executes multiple files at once, so you can focus on making your code readable, without worrying about the implementation. 32 | 33 | Create projects from UI libraries to explorers - with little to no limitations. 34 | 35 | --- 36 | 37 | ### Asset management 38 | 39 | Store all of your UI, modules, and assets locally, and they'll be loaded as Roblox objects before runtime. 40 | 41 | Write your code without waiting for assets. 42 | 43 | ![MidiPlayer example](../assets/images/midi-player-panel-short.svg){ align=right width=200 draggable=false } 44 | 45 | ```lua 46 | local midiPlayer = script:FindFirstAncestor("MidiPlayer") 47 | 48 | local Signal = require(midiPlayer.Util.Signal) 49 | local Date = require(midiPlayer.Util.Date) 50 | local Thread = require(midiPlayer.Util.Thread) 51 | 52 | local gui = midiPlayer.Assets.ScreenGui 53 | 54 | gui.Parent = gethui() 55 | ``` 56 | 57 | --- 58 | 59 | ### Use projects anywhere 60 | 61 | Want to use a resource? Load Rostruct projects in your script with an intelligent Promise-based module system. 62 | 63 | Seamlessly integrate large projects with an easy-to-use API. 64 | 65 | ![Roact example](../assets/images/roact-panel.svg){ align=right width=200 draggable=false } 66 | 67 | ```lua 68 | local Roact = Rostruct.fetchLatest("Roblox", "roact") 69 | :andThen(function(package) 70 | package:build("src/", { Name = "Roact" }) 71 | return package:require(package.tree.Roact) 72 | end) 73 | :expect() 74 | ``` 75 | 76 | --- 77 | 78 | ### Take advantage of model files 79 | 80 | If you're experienced with GitHub, you can set up a workflow to distribute your project as a `*.rbxm` model file. 81 | 82 | Decrease loading times with model files. 83 | 84 | ![Roact example](../assets/images/roact-panel.svg){ align=right width=200 draggable=false } 85 | 86 | ```lua 87 | local Roact = Rostruct.fetchLatest("Roblox", "roact", "Roact.rbxm") 88 | :andThen(function(package) 89 | return package:require( 90 | package:build("Roact.rbxm") 91 | ) 92 | end) 93 | :expect() 94 | ``` 95 | 96 | --- 97 | 98 | ![VS Code logo](../assets/images/vs-code-logo.svg){ align=right width=180 draggable=false } 99 | 100 | ### Test at any time 101 | 102 | Design your project with Rojo, a popular tool used to sync an external code editor with Roblox Studio. 103 | 104 | Write code, even during exploit downtime. 105 | 106 | --- 107 | 108 | ### Recommended tools 109 | 110 | Rostruct can (and should!) be paired with helpful tools like: 111 | 112 | * [Rojo](https://rojo.space/docs/) - a project management tool 113 | * [Roblox LSP](https://devforum.roblox.com/t/roblox-lsp-full-intellisense-for-roblox-and-luau/717745) - full intellisense for Roblox and Luau in VS Code 114 | * [Selene for VS Code](https://marketplace.visualstudio.com/items?itemName=Kampfkarren.selene-vscode) - a static analysis tool to help you write better Lua 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Rostruct logo 3 |

Rostruct

4 | GitHub Actions Release Status 5 | GitHub Actions ESLint Status 6 | Latest Release 7 | Rostruct Documentation 8 |
9 | A modern exploiting solution, built for Roblox and Rojo 10 |
11 | 12 | --- 13 | 14 | **Rostruct** is a project execution framework that runs your code as Roblox instances. This framework substitutes methods that use `HttpGetAsync` and `GetObjects` to load and run code. You can use it with [Rojo](https://rojo.space/) to take advantage of game development tools in your exploits. 15 | 16 | Whether you're familiar with Rojo, want easy asset management, or need some dependencies, you might enjoy using this library. 17 | 18 | See the original concept [here](https://v3rmillion.net/showthread.php?tid=1081675). 19 | 20 | See [rbxm-suite](https://github.com/richie0866/rbxm-suite) for a more efficient solution! 21 | 22 | ## Why Rostruct? 23 | 24 | When it comes to exploiting, projects are often developed and maintained within a single file. However, scripts that get too large become detrimental to your workflow. Over time, your project becomes more difficult to debug, maintain, and share. 25 | 26 | In contrast, with Rojo, your codebase gets turned directly into Roblox Instances. Taking this modular approach to exploiting can significantly improve the development experience. 27 | 28 | Rostruct's design complements a Rojo-based workflow, introducing script developers to a professional way to manage projects. 29 | 30 | ## [Usage](https://richie0866.github.io/Rostruct) 31 | 32 | Documentation is available at the [GitHub Pages site](https://richie0866.github.io/Rostruct). 33 | 34 | ## How it works 35 | 36 | Rostruct Build Example 38 | 39 | Rostruct builds instances following a [file conversion model](https://richie0866.github.io/Rostruct/api-reference/file-conversion/). Files compatible with Rostruct (`lua`, `json`, `rbxm`, etc.) are turned into Roblox instances. 40 | 41 | Scripts have preset `script` and `require` globals to mirror LocalScript and ModuleScript objects. This way, runtime is similar between Roblox Studio and script executors. 42 | 43 | ## Features 44 | 45 | * Promotes modular programming 46 | * Keep your codebase readable and maintainable. 47 | 48 | * Instance-based execution 49 | * Write your code like it's Roblox Studio. 50 | 51 | * GitHub support 52 | * Build packages from GitHub releases, allowing users to execute your code without manually downloading it. 53 | 54 | * Builds `rbxm` models 55 | * Go `GetObjects`-free by including assets in your project files. 56 | 57 | * Designed for [Rojo](https://github.com/rojo-rbx/rojo#readme) 58 | * Test your code in Roblox Studio without an exploit. 59 | 60 | ## Contributing 61 | 62 | If there are any features you think are missing, or the code can be improved, feel free to [open an issue](https://github.com/richie0866/Rostruct/issues)! 63 | 64 | If you'd like to contribute, [fork Rostruct](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and submit a [pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) when you're ready. 65 | 66 | ### Setup 67 | 68 | 1. Run `npm install` to install all dependencies used by this project. 69 | 2. Then, run `rbxtsc -w` to start building the TypeScript source to Lua files as you make changes. 70 | 71 | ### Testing 72 | 73 | 3. Run `npm run build:prod` in a Git Bash terminal to generate an output file. Make sure the `out/` directory is up-to-date with `rbxtsc`! 74 | 4. The command should create a `Rostruct.lua` file in the root directory. At the end of the script, you'll find this code: 75 | 76 | ```lua 77 | return Rostruct 78 | ``` 79 | You can replace that line with code that uses Rostruct. 80 | 81 | ## License 82 | 83 | Rostruct is available under the MIT license. See [LICENSE](https://github.com/richie0866/Rostruct/blob/main/LICENSE) for more details. 84 | -------------------------------------------------------------------------------- /docs/getting-started/publishing-your-project.md: -------------------------------------------------------------------------------- 1 | # Publishing your project 2 | 3 | Once you're ready to distribute your project, it's important to note that it should run automatically. Ideally, the end-user shouldn't do more than save a script. 4 | 5 | Fortunately, Rostruct provides functionality to deploy your codebase through GitHub: 6 | 7 | ## Deploying from GitHub 8 | 9 | The best way to publish your Rostruct project is by creating [GitHub Releases](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository) in a repository. Rostruct will save and version-check the releases for you. 10 | 11 | You should write a **retriever** script that lets users run your project without any extra setup. 12 | The **retriever** handles the execution of your repo's latest GitHub Release. However, you'll need to load Rostruct to do that. 13 | 14 | ## Loading Rostruct 15 | 16 | In the retriever, you can load Rostruct in two ways: with an HTTP request, or from Rostruct's source code. Each option has its pros and cons, so choose whichever one best fits your project. 17 | 18 | ### with HTTP GET recommended { data-toc-label="with HTTP GET" } 19 | 20 | If you prefer a quick and concise way to load Rostruct, you can load it with an HTTP request. 21 | 22 | To do this, you should pick a release from the [GitHub Releases page](https://github.com/richie0866/Rostruct/releases/latest), and copy the **tag version** to `TAG_VERSION_HERE` in this code: 23 | 24 | ```lua hl_lines="3" 25 | local Rostruct = loadstring(game:HttpGetAsync( 26 | "https://github.com/richie0866/Rostruct/releases/download/" 27 | .. "TAG_VERSION_HERE" 28 | .. "/Rostruct.lua" 29 | ))() 30 | ``` 31 | 32 | This loads the Rostruct library by getting the source and executing it. You can now [deploy your project](#deployment). 33 | 34 | ### with source code 35 | 36 | If you don't want to make an HTTP request, you can load Rostruct instantly by using the source code in your script. In other words, you'll be using Rostruct as an **internal module**. 37 | 38 | To add Rostruct's source to your retriever, download the `Rostruct.lua` asset from your preferred release of Rostruct from the [GitHub Releases page](https://github.com/richie0866/Rostruct/releases/latest), and paste it into your retriever. 39 | 40 | This file should end with `#!lua return Rostruct`. Since you're going to use Rostruct, all you have to do is remove that line! 41 | 42 | Although this bloats up your file, unlike the first method, you can use Rostruct immediately. 43 | 44 | ## Running your project 45 | 46 | After you've loaded Rostruct, use [`Rostruct.fetch`](../api-reference/rostruct/fetch.md) or [`Rostruct.fetchLatest`](../api-reference/rostruct/fetchlatest.md) to download and package the release files in your retriever. 47 | 48 | ### Deployment 49 | 50 | You can deploy your project using Rostruct's `fetch` functions, which return a Promise that resolves with a new [`Package`](../api-reference/package/properties.md) object. It functions almost identically to using `Rostruct.open`, just with a Promise: 51 | 52 | === "Start" 53 | 54 | ```lua 55 | -- Download the latest release to local files 56 | return Rostruct.fetchLatest("richie0866", "MidiPlayer") 57 | -- Then, build and start all scripts 58 | :andThen(function(package) 59 | package:build("src/") 60 | package:start() 61 | return package 62 | end) 63 | -- Finally, wait until the Promise is done 64 | :expect() 65 | ``` 66 | 67 | === "Require" 68 | 69 | ```lua 70 | -- Download the latest release of Roact to local files 71 | return Rostruct.fetchLatest("Roblox", "roact") 72 | -- Then, build and require Roact 73 | :andThen(function(package) 74 | return package:require( 75 | package:build("src/", { Name = "Roact" }) 76 | ) 77 | end) 78 | -- Finally, wait until the Promise is done, and 79 | -- return the result of package:require 80 | :expect() 81 | ``` 82 | 83 | Now, anyone with this script can deploy your project in a Roblox script executor. Remember to test your code! 84 | 85 | You can simplify the script for end-user by saving the retriever in your repo and loading its source with `HttpGetAsync`: 86 | 87 | ### Distribution 88 | 89 | Some users may prefer a short and concise way to use your project. To account for this, you should provide additional code that uses the `loadstring-HttpGet` pattern to run your project's retriever. 90 | 91 | Loading your module through an HTTP request may seem counterproductive, but some developers may prefer it in a single-file project. So, your code should look something like this: 92 | 93 | ```lua 94 | local Foo = loadstring(game:HttpGetAsync("LINK_TO_RAW_RETRIEVER"))() 95 | ``` 96 | 97 | `RAW_RETRIEVER_URL` should be replaced with a link to your retriever's raw contents. It may be in your best interest to [load Rostruct internally](#with-source-code) to avoid the extra HTTP request. 98 | -------------------------------------------------------------------------------- /src/modules/make/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | declare type WritablePropertyNames = { 4 | readonly [K in keyof T]-?: T[K] extends Callback 5 | ? never 6 | : (() => F extends { 7 | [Q in K]: T[K]; 8 | } 9 | ? 1 10 | : 2) extends () => F extends { 11 | -readonly [Q in K]: T[K]; 12 | } 13 | ? 1 14 | : 2 15 | ? K 16 | : never; 17 | }[keyof T]; 18 | declare type GetBindableToRBXScriptSignal = { 19 | [key in { 20 | [K in keyof T]-?: T[K] extends RBXScriptSignal ? K : never; 21 | }[keyof T]]: T[key] extends RBXScriptSignal ? R : never; 22 | }; 23 | /** 24 | * Returns a table wherein an object's writable properties can be specified, 25 | * while also allowing functions to be passed in which can be bound to a RBXScriptSignal. 26 | */ 27 | declare type GetPartialObjectWithBindableConnectSlots = Partial< 28 | Pick> & GetBindableToRBXScriptSignal 29 | >; 30 | /** 31 | * Instantiates a new Instance of `className` with given `settings`, 32 | * where `settings` is an object of the form { [K: propertyName]: value }. 33 | * 34 | * `settings.Children` is an array of child objects to be parented to the generated Instance. 35 | * 36 | * Events can be set to a callback function, which will be connected. 37 | * 38 | * `settings.Parent` is always set last. 39 | */ 40 | declare function Make< 41 | T extends keyof CreatableInstances, 42 | Q extends GetPartialObjectWithBindableConnectSlots & { 43 | /** The Children to place inside of this Instance. */ 44 | Children?: ReadonlyArray; 45 | Parent?: Instance | undefined; 46 | }, 47 | >( 48 | className: T, 49 | settings: Q, 50 | ): CreatableInstances[T] & 51 | { 52 | [K_1 in keyof ({ [O in Extract<"Name", keyof Q>]: Q[O] } & { 53 | ClassName: T; 54 | } & (Q["Children"] extends never 55 | ? never 56 | : { 57 | [K in Exclude< 58 | keyof Q["Children"], 59 | | number 60 | | "find" 61 | | "size" 62 | | "isEmpty" 63 | | "join" 64 | | "move" 65 | | "includes" 66 | | "indexOf" 67 | | "every" 68 | | "some" 69 | | "forEach" 70 | | "map" 71 | | "mapFiltered" 72 | | "filterUndefined" 73 | | "filter" 74 | | "reduce" 75 | | "findIndex" 76 | | "_nominal_Array" 77 | | "length" 78 | >]: Q["Children"][K] extends infer A 79 | ? A extends { 80 | Name: string; 81 | } 82 | ? string extends A["Name"] 83 | ? never 84 | : (k: { [P in A["Name"]]: A }) => void 85 | : never 86 | : never; 87 | }[Exclude< 88 | keyof Q["Children"], 89 | | number 90 | | "find" 91 | | "size" 92 | | "isEmpty" 93 | | "join" 94 | | "move" 95 | | "includes" 96 | | "indexOf" 97 | | "every" 98 | | "some" 99 | | "forEach" 100 | | "map" 101 | | "mapFiltered" 102 | | "filterUndefined" 103 | | "filter" 104 | | "reduce" 105 | | "findIndex" 106 | | "_nominal_Array" 107 | | "length" 108 | >] extends (k: infer U) => void 109 | ? U 110 | : never))]: ({ [O in Extract<"Name", keyof Q>]: Q[O] } & { 111 | ClassName: T; 112 | } & (Q["Children"] extends never 113 | ? never 114 | : { 115 | [K in Exclude< 116 | keyof Q["Children"], 117 | | number 118 | | "find" 119 | | "size" 120 | | "isEmpty" 121 | | "join" 122 | | "move" 123 | | "includes" 124 | | "indexOf" 125 | | "every" 126 | | "some" 127 | | "forEach" 128 | | "map" 129 | | "mapFiltered" 130 | | "filterUndefined" 131 | | "filter" 132 | | "reduce" 133 | | "findIndex" 134 | | "_nominal_Array" 135 | | "length" 136 | >]: Q["Children"][K] extends infer A 137 | ? A extends { 138 | Name: string; 139 | } 140 | ? string extends A["Name"] 141 | ? never 142 | : (k: { [P in A["Name"]]: A }) => void 143 | : never 144 | : never; 145 | }[Exclude< 146 | keyof Q["Children"], 147 | | number 148 | | "find" 149 | | "size" 150 | | "isEmpty" 151 | | "join" 152 | | "move" 153 | | "includes" 154 | | "indexOf" 155 | | "every" 156 | | "some" 157 | | "forEach" 158 | | "map" 159 | | "mapFiltered" 160 | | "filterUndefined" 161 | | "filter" 162 | | "reduce" 163 | | "findIndex" 164 | | "_nominal_Array" 165 | | "length" 166 | >] extends (k: infer U) => void 167 | ? U 168 | : never))[K_1]; 169 | }; 170 | export = Make; 171 | -------------------------------------------------------------------------------- /docs/assets/overrides/home.html: -------------------------------------------------------------------------------- 1 | {% extends "main.html" %} 2 | 3 | 4 | {% block tabs %} 5 | {{ super() }} 6 | 7 | 8 | 236 | 237 | 238 |
239 |
240 |
241 |
242 | 243 |
244 |
245 |

Take exploiting to the next level

246 |

Manage your Lua projects using professional-grade tools to run and publish your code. Deploy your project files to a Roblox script executor with a simple and flexible execution library.

247 | 248 | Get started 249 | 250 | 251 | Go to GitHub 252 | 253 |
254 |
255 |
256 |
257 | 258 | {% endblock %} 259 | 260 | 261 | {% block content %}{% endblock %} 262 | 263 | 264 | {% block footer %}{% endblock %} 265 | -------------------------------------------------------------------------------- /src/core/VirtualScript.ts: -------------------------------------------------------------------------------- 1 | import { Store } from "./Store"; 2 | import { HttpService } from "modules/services"; 3 | import type { Executor, VirtualEnvironment } from "./types"; 4 | 5 | /** Maps scripts to the module they're loading, like a history of `[script who loaded]: module` */ 6 | const currentlyLoading = new Map(); 7 | 8 | /** 9 | * Check if a module contains a cyclic dependency chain. 10 | * @param module The starting VirtualScript. 11 | */ 12 | function checkTraceback(module: VirtualScript) { 13 | let currentModule: VirtualScript | undefined = module; 14 | let depth = 0; 15 | while (currentModule) { 16 | depth += 1; 17 | currentModule = currentlyLoading.get(currentModule); 18 | 19 | // If the loop reaches 'module' again, this is a cyclic reference. 20 | if (module === currentModule) { 21 | let traceback = module.getChunkName(); 22 | 23 | // Create a string to represent the dependency chain. 24 | for (let i = 0; i < depth; i++) { 25 | currentModule = currentlyLoading.get(currentModule)!; 26 | traceback += `\n\t\t⇒ ${currentModule.getChunkName()}`; 27 | } 28 | 29 | throw ( 30 | `Requested module '${module.getChunkName()}' contains a cyclic reference` + 31 | `\n\tTraceback: ${traceback}` 32 | ); 33 | } 34 | } 35 | } 36 | 37 | /** Manages file execution. */ 38 | export class VirtualScript { 39 | /** Maps VirtualScripts to their instances. */ 40 | private static readonly fromInstance = Store.getStore("VirtualScriptStore"); 41 | 42 | /** An identifier for this VirtualScript. */ 43 | public readonly id = "VirtualScript-" + HttpService.GenerateGUID(false); 44 | 45 | /** The function to be called at runtime when the script runs or gets required. */ 46 | private executor?: Executor; 47 | 48 | /** The executor's return value after being called the first time. ModuleScripts must have a non-nil result. */ 49 | private result?: unknown; 50 | 51 | /** Whether the executor has already been called. */ 52 | private jobComplete = false; 53 | 54 | /** A custom environment used during runtime. */ 55 | private readonly scriptEnvironment: VirtualEnvironment; 56 | 57 | constructor( 58 | /** The Instance that represents this object used for globals. */ 59 | public readonly instance: LuaSourceContainer, 60 | 61 | /** The file this object extends. */ 62 | public readonly path: string, 63 | 64 | /** The root directory. */ 65 | public readonly root: string, 66 | 67 | /** The contents of the file. */ 68 | public readonly source = readfile(path), 69 | ) { 70 | this.scriptEnvironment = setmetatable( 71 | { 72 | script: instance, 73 | require: (obj: ModuleScript) => VirtualScript.loadModule(obj, this), 74 | _PATH: path, 75 | _ROOT: root, 76 | }, 77 | { 78 | __index: getfenv(0) as never, 79 | __metatable: "This metatable is locked", 80 | }, 81 | ); 82 | VirtualScript.fromInstance.set(instance, this); 83 | } 84 | 85 | /** 86 | * Gets an existing VirtualScript for a specific instance. 87 | * @param object 88 | */ 89 | public static getFromInstance(object: LuaSourceContainer): VirtualScript | undefined { 90 | return this.fromInstance.get(object); 91 | } 92 | 93 | /** 94 | * Executes a `VirtualScript` from the given module and returns the result. 95 | * @param object The ModuleScript to require. 96 | * @returns What the module returned. 97 | */ 98 | public static requireFromInstance(object: ModuleScript): unknown { 99 | const module = this.getFromInstance(object); 100 | assert(module, `Failed to get VirtualScript for Instance '${object.GetFullName()}'`); 101 | return module.runExecutor(); 102 | } 103 | 104 | /** 105 | * Requires a `ModuleScript`. If the module has a `VirtualScript` counterpart, 106 | * it calls `VirtualScript.execute` and returns the result. 107 | * 108 | * Detects recursive references using roblox-ts's RuntimeLib solution. 109 | * The original source of this module can be found in the link below, as well as the license: 110 | * - Source: https://github.com/roblox-ts/roblox-ts/blob/master/lib/RuntimeLib.lua 111 | * - License: https://github.com/roblox-ts/roblox-ts/blob/master/LICENSE 112 | * 113 | * @param object The ModuleScript to require. 114 | * @param caller The calling VirtualScript. 115 | * @returns What the module returned. 116 | */ 117 | private static loadModule(object: ModuleScript, caller: VirtualScript): unknown { 118 | const module = this.fromInstance.get(object); 119 | if (!module) return require(object); 120 | 121 | currentlyLoading.set(caller, module); 122 | 123 | // Check for a cyclic dependency 124 | checkTraceback(module); 125 | 126 | const result = module.runExecutor(); 127 | 128 | // Thread-safe cleanup avoids overwriting other loading modules 129 | if (currentlyLoading.get(caller) === module) currentlyLoading.delete(caller); 130 | 131 | return result; 132 | } 133 | 134 | /** 135 | * Returns the chunk name for the module for traceback. 136 | */ 137 | public getChunkName() { 138 | const file = this.path.sub(this.root.size() + 1); 139 | return `@${file} (${this.instance.GetFullName()})`; 140 | } 141 | 142 | /** 143 | * Sets the executor function. 144 | * @param exec The function to call on execution. 145 | */ 146 | public setExecutor(exec: Executor) { 147 | assert(this.jobComplete === false, "Cannot set executor after script was executed"); 148 | this.executor = exec; 149 | } 150 | 151 | /** 152 | * Gets or creates a new executor function, and returns the executor function. 153 | * The executor is automatically given a special global environment. 154 | * @returns The executor function. 155 | */ 156 | public createExecutor(): Executor { 157 | if (this.executor) return this.executor; 158 | const [f, err] = loadstring(this.source, `=${this.getChunkName()}`); 159 | assert(f, err); 160 | return (this.executor = setfenv(f, this.scriptEnvironment)); 161 | } 162 | 163 | /** 164 | * Runs the executor function if not already run and returns results. 165 | * @returns The value returned by the executor. 166 | */ 167 | public runExecutor(): T { 168 | if (this.jobComplete) return this.result as never; 169 | 170 | const result = this.createExecutor()(this.scriptEnvironment); 171 | 172 | if (this.instance.IsA("ModuleScript") && result === undefined) 173 | throw `Module '${this.getChunkName()}' did not return any value`; 174 | 175 | this.jobComplete = true; 176 | 177 | return (this.result = result) as never; 178 | } 179 | 180 | /** 181 | * Runs the executor function if not already run and returns results. 182 | * @returns A promise which resolves with the value returned by the executor. 183 | */ 184 | public deferExecutor(): Promise { 185 | return Promise.defer((resolve) => resolve(this.runExecutor())).timeout( 186 | 30, 187 | `Script ${this.getChunkName()} reached execution timeout! Try not to yield the main thread in LocalScripts.`, 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /bin/runtime.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Originally RuntimeLib.lua supplied by roblox-ts, modified for use when bundled. 3 | The original source of this module can be found in the link below, as well as the license: 4 | 5 | https://github.com/roblox-ts/roblox-ts/blob/master/lib/RuntimeLib.lua 6 | https://github.com/roblox-ts/roblox-ts/blob/master/LICENSE 7 | ]] 8 | 9 | local TS = { 10 | _G = {}; 11 | } 12 | 13 | setmetatable(TS, { 14 | __index = function(self, k) 15 | if k == "Promise" then 16 | self.Promise = TS.initialize("modules", "Promise") 17 | return self.Promise 18 | end 19 | end 20 | }) 21 | 22 | -- Runtime classes 23 | local FilePtr 24 | do 25 | FilePtr = {} 26 | FilePtr.__index = FilePtr 27 | 28 | function FilePtr.new(path) 29 | local fileName, slash = string.match(path, "([^/]+)(/*)$") 30 | return setmetatable({ 31 | name = fileName, 32 | path = string.sub(path, 1, -#fileName - (slash ~= "" and 2 or 1)), 33 | }, FilePtr) 34 | end 35 | 36 | function FilePtr:__index(k) 37 | if k == "Parent" then 38 | return FilePtr.new(self.path) 39 | end 40 | end 41 | 42 | end 43 | 44 | local Module 45 | do 46 | Module = {} 47 | Module.__index = Module 48 | 49 | function Module.new(path, name, func) 50 | return setmetatable({ 51 | -- Init files are representations of their parent directory, 52 | -- so if it's an init file, we trim the "/init.lua" off of 53 | -- the file path. 54 | path = name ~= "init" 55 | and path 56 | or FilePtr.new(path).path, 57 | name = name, 58 | func = func, 59 | data = nil; 60 | }, Module) 61 | end 62 | 63 | function Module:__index(k) 64 | if Module[k] then 65 | return Module[k] 66 | elseif k == "Parent" then 67 | return FilePtr.new(self.path) 68 | elseif k == "Name" then 69 | return self.path 70 | end 71 | end 72 | 73 | function Module:require() 74 | if self.func then 75 | self.data = self.func() 76 | self.func = nil 77 | end 78 | return self.data 79 | end 80 | 81 | function Module:GetFullName() 82 | return self.path 83 | end 84 | 85 | end 86 | 87 | local Symbol 88 | do 89 | Symbol = {} 90 | Symbol.__index = Symbol 91 | setmetatable( 92 | Symbol, 93 | { 94 | __call = function(_, description) 95 | local self = setmetatable({}, Symbol) 96 | self.description = "Symbol(" .. (description or "") .. ")" 97 | return self 98 | end, 99 | } 100 | ) 101 | 102 | local symbolRegistry = setmetatable( 103 | {}, 104 | { 105 | __index = function(self, k) 106 | self[k] = Symbol(k) 107 | return self[k] 108 | end, 109 | } 110 | ) 111 | 112 | function Symbol:toString() 113 | return self.description 114 | end 115 | 116 | Symbol.__tostring = Symbol.toString 117 | 118 | -- Symbol.for 119 | function Symbol.getFor(key) 120 | return symbolRegistry[key] 121 | end 122 | 123 | function Symbol.keyFor(goalSymbol) 124 | for key, symbol in pairs(symbolRegistry) do 125 | if symbol == goalSymbol then 126 | return key 127 | end 128 | end 129 | end 130 | end 131 | 132 | TS.Symbol = Symbol 133 | TS.Symbol_iterator = Symbol("Symbol.iterator") 134 | 135 | -- Provides a way to attribute modules to files. 136 | local modulesByPath = {} 137 | local modulesByName = {} 138 | 139 | -- Bundle compatibility 140 | function TS.register(path, name, func) 141 | local module = Module.new(path, name, func) 142 | modulesByPath[path] = module 143 | modulesByName[name] = module 144 | return module 145 | end 146 | 147 | function TS.get(path) 148 | return modulesByPath[path] 149 | end 150 | 151 | function TS.initialize(...) 152 | local symbol = setmetatable({}, {__tostring = function() 153 | return "root" 154 | end}) 155 | local caller = TS.register(symbol, symbol) 156 | return TS.import(caller, { path = "out/" }, ...) 157 | end 158 | 159 | -- module resolution 160 | function TS.getModule(_object, _moduleName) 161 | return error("TS.getModule is not supported", 2) 162 | end 163 | 164 | -- This is a hash which TS.import uses as a kind of linked-list-like history of [Script who Loaded] -> Library 165 | local currentlyLoading = {} 166 | local registeredLibraries = {} 167 | 168 | function TS.import(caller, parentPtr, ...) 169 | -- Because 'Module.Parent' returns a FilePtr, the module handles the indexing. 170 | -- Getting 'parentPtr.path' will return the result of FilePtr.Parent.Parent... 171 | local modulePath = parentPtr.path .. table.concat({...}, "/") .. ".lua" 172 | local moduleInit = parentPtr.path .. table.concat({...}, "/") .. "/init.lua" 173 | local module = assert( 174 | modulesByPath[modulePath] or modulesByPath[moduleInit], 175 | "No module exists at path '" .. modulePath .. "'" 176 | ) 177 | 178 | currentlyLoading[caller] = module 179 | 180 | -- Check to see if a case like this occurs: 181 | -- module -> Module1 -> Module2 -> module 182 | 183 | -- WHERE currentlyLoading[module] is Module1 184 | -- and currentlyLoading[Module1] is Module2 185 | -- and currentlyLoading[Module2] is module 186 | 187 | local currentModule = module 188 | local depth = 0 189 | 190 | while currentModule do 191 | depth = depth + 1 192 | currentModule = currentlyLoading[currentModule] 193 | 194 | if currentModule == module then 195 | local str = currentModule.name -- Get the string traceback 196 | 197 | for _ = 1, depth do 198 | currentModule = currentlyLoading[currentModule] 199 | str ..= " => " .. currentModule.name 200 | end 201 | 202 | error("Failed to import! Detected a circular dependency chain: " .. str, 2) 203 | end 204 | end 205 | 206 | if not registeredLibraries[module] then 207 | if TS._G[module] then 208 | error( 209 | "Invalid module access! Do you have two TS runtimes trying to import this? " .. module.path, 210 | 2 211 | ) 212 | end 213 | 214 | TS._G[module] = TS 215 | registeredLibraries[module] = true -- register as already loaded for subsequent calls 216 | end 217 | 218 | local data = module:require() 219 | 220 | if currentlyLoading[caller] == module then -- Thread-safe cleanup! 221 | currentlyLoading[caller] = nil 222 | end 223 | 224 | return data 225 | end 226 | 227 | -- general utility functions 228 | function TS.async(callback) 229 | local Promise = TS.Promise 230 | return function(...) 231 | local n = select("#", ...) 232 | local args = { ... } 233 | return Promise.new(function(resolve, reject) 234 | coroutine.wrap(function() 235 | local ok, result = pcall(callback, unpack(args, 1, n)) 236 | if ok then 237 | resolve(result) 238 | else 239 | reject(result) 240 | end 241 | end)() 242 | end) 243 | end 244 | end 245 | 246 | function TS.await(promise) 247 | local Promise = TS.Promise 248 | if not Promise.is(promise) then 249 | return promise 250 | end 251 | 252 | local status, value = promise:awaitStatus() 253 | if status == Promise.Status.Resolved then 254 | return value 255 | elseif status == Promise.Status.Rejected then 256 | error(value, 2) 257 | else 258 | error("The awaited Promise was cancelled", 2) 259 | end 260 | end 261 | 262 | -- opcall 263 | 264 | function TS.opcall(func, ...) 265 | local success, valueOrErr = pcall(func, ...) 266 | if success then 267 | return { 268 | success = true, 269 | value = valueOrErr, 270 | } 271 | else 272 | return { 273 | success = false, 274 | error = valueOrErr, 275 | } 276 | end 277 | end 278 | -------------------------------------------------------------------------------- /src/modules/zzlib/init.lua: -------------------------------------------------------------------------------- 1 | -- zzlib - zlib decompression in Lua - Implementation-independent code 2 | 3 | -- Copyright (c) 2016-2020 Francois Galea 4 | -- This program is free software. It comes without any warranty, to 5 | -- the extent permitted by applicable law. You can redistribute it 6 | -- and/or modify it under the terms of the Do What The Fuck You Want 7 | -- To Public License, Version 2, as published by Sam Hocevar. See 8 | -- the COPYING file or http://www.wtfpl.net/ for more details. 9 | 10 | 11 | local unpack = unpack 12 | local result 13 | 14 | local infl do 15 | local inflate = {} 16 | 17 | local bit = bit32 18 | 19 | inflate.band = bit.band 20 | inflate.rshift = bit.rshift 21 | 22 | function inflate.bitstream_init(file) 23 | local bs = { 24 | file = file, -- the open file handle 25 | buf = nil, -- character buffer 26 | len = nil, -- length of character buffer 27 | pos = 1, -- position in char buffer 28 | b = 0, -- bit buffer 29 | n = 0, -- number of bits in buffer 30 | } 31 | -- get rid of n first bits 32 | function bs:flushb(n) 33 | self.n = self.n - n 34 | self.b = bit.rshift(self.b,n) 35 | end 36 | -- peek a number of n bits from stream 37 | function bs:peekb(n) 38 | while self.n < n do 39 | if self.pos > self.len then 40 | self.buf = self.file:read(4096) 41 | self.len = self.buf:len() 42 | self.pos = 1 43 | end 44 | self.b = self.b + bit.lshift(self.buf:byte(self.pos),self.n) 45 | self.pos = self.pos + 1 46 | self.n = self.n + 8 47 | end 48 | return bit.band(self.b,bit.lshift(1,n)-1) 49 | end 50 | -- get a number of n bits from stream 51 | function bs:getb(n) 52 | local ret = bs:peekb(n) 53 | self.n = self.n - n 54 | self.b = bit.rshift(self.b,n) 55 | return ret 56 | end 57 | -- get next variable-size of maximum size=n element from stream, according to Huffman table 58 | function bs:getv(hufftable,n) 59 | local e = hufftable[bs:peekb(n)] 60 | local len = bit.band(e,15) 61 | local ret = bit.rshift(e,4) 62 | self.n = self.n - len 63 | self.b = bit.rshift(self.b,len) 64 | return ret 65 | end 66 | function bs:close() 67 | if self.file then 68 | self.file:close() 69 | end 70 | end 71 | if type(file) == "string" then 72 | bs.file = nil 73 | bs.buf = file 74 | else 75 | bs.buf = file:read(4096) 76 | end 77 | bs.len = bs.buf:len() 78 | return bs 79 | end 80 | 81 | local function hufftable_create(depths) 82 | local nvalues = #depths 83 | local nbits = 1 84 | local bl_count = {} 85 | local next_code = {} 86 | for i=1,nvalues do 87 | local d = depths[i] 88 | if d > nbits then 89 | nbits = d 90 | end 91 | bl_count[d] = (bl_count[d] or 0) + 1 92 | end 93 | local table = {} 94 | local code = 0 95 | bl_count[0] = 0 96 | for i=1,nbits do 97 | code = (code + (bl_count[i-1] or 0)) * 2 98 | next_code[i] = code 99 | end 100 | for i=1,nvalues do 101 | local len = depths[i] or 0 102 | if len > 0 then 103 | local e = (i-1)*16 + len 104 | local code = next_code[len] 105 | local rcode = 0 106 | for j=1,len do 107 | rcode = rcode + bit.lshift(bit.band(1,bit.rshift(code,j-1)),len-j) 108 | end 109 | for j=0,2^nbits-1,2^len do 110 | table[j+rcode] = e 111 | end 112 | next_code[len] = next_code[len] + 1 113 | end 114 | end 115 | return table,nbits 116 | end 117 | 118 | local function block_loop(out,bs,nlit,ndist,littable,disttable) 119 | local lit 120 | repeat 121 | lit = bs:getv(littable,nlit) 122 | if lit < 256 then 123 | table.insert(out,lit) 124 | elseif lit > 256 then 125 | local nbits = 0 126 | local size = 3 127 | local dist = 1 128 | if lit < 265 then 129 | size = size + lit - 257 130 | elseif lit < 285 then 131 | nbits = bit.rshift(lit-261,2) 132 | size = size + bit.lshift(bit.band(lit-261,3)+4,nbits) 133 | else 134 | size = 258 135 | end 136 | if nbits > 0 then 137 | size = size + bs:getb(nbits) 138 | end 139 | local v = bs:getv(disttable,ndist) 140 | if v < 4 then 141 | dist = dist + v 142 | else 143 | nbits = bit.rshift(v-2,1) 144 | dist = dist + bit.lshift(bit.band(v,1)+2,nbits) 145 | dist = dist + bs:getb(nbits) 146 | end 147 | local p = #out-dist+1 148 | while size > 0 do 149 | table.insert(out,out[p]) 150 | p = p + 1 151 | size = size - 1 152 | end 153 | end 154 | until lit == 256 155 | end 156 | 157 | local function block_dynamic(out,bs) 158 | local order = { 17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16 } 159 | local hlit = 257 + bs:getb(5) 160 | local hdist = 1 + bs:getb(5) 161 | local hclen = 4 + bs:getb(4) 162 | local depths = {} 163 | for i=1,hclen do 164 | local v = bs:getb(3) 165 | depths[order[i]] = v 166 | end 167 | for i=hclen+1,19 do 168 | depths[order[i]] = 0 169 | end 170 | local lengthtable,nlen = hufftable_create(depths) 171 | local i=1 172 | while i<=hlit+hdist do 173 | local v = bs:getv(lengthtable,nlen) 174 | if v < 16 then 175 | depths[i] = v 176 | i = i + 1 177 | elseif v < 19 then 178 | local nbt = {2,3,7} 179 | local nb = nbt[v-15] 180 | local c = 0 181 | local n = 3 + bs:getb(nb) 182 | if v == 16 then 183 | c = depths[i-1] 184 | elseif v == 18 then 185 | n = n + 8 186 | end 187 | for j=1,n do 188 | depths[i] = c 189 | i = i + 1 190 | end 191 | else 192 | error("wrong entry in depth table for literal/length alphabet: "..v); 193 | end 194 | end 195 | local litdepths = {} for i=1,hlit do table.insert(litdepths,depths[i]) end 196 | local littable,nlit = hufftable_create(litdepths) 197 | local distdepths = {} for i=hlit+1,#depths do table.insert(distdepths,depths[i]) end 198 | local disttable,ndist = hufftable_create(distdepths) 199 | block_loop(out,bs,nlit,ndist,littable,disttable) 200 | end 201 | 202 | local function block_static(out,bs) 203 | local cnt = { 144, 112, 24, 8 } 204 | local dpt = { 8, 9, 7, 8 } 205 | local depths = {} 206 | for i=1,4 do 207 | local d = dpt[i] 208 | for j=1,cnt[i] do 209 | table.insert(depths,d) 210 | end 211 | end 212 | local littable,nlit = hufftable_create(depths) 213 | depths = {} 214 | for i=1,32 do 215 | depths[i] = 5 216 | end 217 | local disttable,ndist = hufftable_create(depths) 218 | block_loop(out,bs,nlit,ndist,littable,disttable) 219 | end 220 | 221 | local function block_uncompressed(out,bs) 222 | bs:flushb(bit.band(bs.n,7)) 223 | local len = bs:getb(16) 224 | if bs.n > 0 then 225 | error("Unexpected.. should be zero remaining bits in buffer.") 226 | end 227 | local nlen = bs:getb(16) 228 | if bit.bxor(len,nlen) ~= 65535 then 229 | error("LEN and NLEN don't match") 230 | end 231 | for i=bs.pos,bs.pos+len-1 do 232 | table.insert(out,bs.buf:byte(i,i)) 233 | end 234 | bs.pos = bs.pos + len 235 | end 236 | 237 | function inflate.main(bs) 238 | local last,type 239 | local output = {} 240 | repeat 241 | local block 242 | last = bs:getb(1) 243 | type = bs:getb(2) 244 | if type == 0 then 245 | block_uncompressed(output,bs) 246 | elseif type == 1 then 247 | block_static(output,bs) 248 | elseif type == 2 then 249 | block_dynamic(output,bs) 250 | else 251 | error("unsupported block type") 252 | end 253 | until last == 1 254 | bs:flushb(bit.band(bs.n,7)) 255 | return output 256 | end 257 | 258 | local crc32_table 259 | function inflate.crc32(s,crc) 260 | if not crc32_table then 261 | crc32_table = {} 262 | for i=0,255 do 263 | local r=i 264 | for j=1,8 do 265 | r = bit.bxor(bit.rshift(r,1),bit.band(0xedb88320,bit.bnot(bit.band(r,1)-1))) 266 | end 267 | crc32_table[i] = r 268 | end 269 | end 270 | crc = bit.bnot(crc or 0) 271 | for i=1,#s do 272 | local c = s:byte(i) 273 | crc = bit.bxor(crc32_table[bit.bxor(c,bit.band(crc,0xff))],bit.rshift(crc,8)) 274 | end 275 | crc = bit.bnot(crc) 276 | if crc<0 then 277 | -- in Lua < 5.2, sign extension was performed 278 | crc = crc + 4294967296 279 | end 280 | return crc 281 | end 282 | 283 | infl = inflate 284 | end 285 | 286 | local zzlib = {} 287 | 288 | local function arraytostr(array) 289 | local tmp = {} 290 | local size = #array 291 | local pos = 1 292 | local imax = 1 293 | while size > 0 do 294 | local bsize = size>=2048 and 2048 or size 295 | local s = string.char(unpack(array,pos,pos+bsize-1)) 296 | pos = pos + bsize 297 | size = size - bsize 298 | local i = 1 299 | while tmp[i] do 300 | s = tmp[i]..s 301 | tmp[i] = nil 302 | i = i + 1 303 | end 304 | if i > imax then 305 | imax = i 306 | end 307 | tmp[i] = s 308 | end 309 | local str = "" 310 | for i=1,imax do 311 | if tmp[i] then 312 | str = tmp[i]..str 313 | end 314 | end 315 | return str 316 | end 317 | 318 | local function inflate_gzip(bs) 319 | local id1,id2,cm,flg = bs.buf:byte(1,4) 320 | if id1 ~= 31 or id2 ~= 139 then 321 | error("invalid gzip header") 322 | end 323 | if cm ~= 8 then 324 | error("only deflate format is supported") 325 | end 326 | bs.pos=11 327 | if infl.band(flg,4) ~= 0 then 328 | local xl1,xl2 = bs.buf.byte(bs.pos,bs.pos+1) 329 | local xlen = xl2*256+xl1 330 | bs.pos = bs.pos+xlen+2 331 | end 332 | if infl.band(flg,8) ~= 0 then 333 | local pos = bs.buf:find("\0",bs.pos) 334 | bs.pos = pos+1 335 | end 336 | if infl.band(flg,16) ~= 0 then 337 | local pos = bs.buf:find("\0",bs.pos) 338 | bs.pos = pos+1 339 | end 340 | if infl.band(flg,2) ~= 0 then 341 | -- TODO: check header CRC16 342 | bs.pos = bs.pos+2 343 | end 344 | local result = arraytostr(infl.main(bs)) 345 | local crc = bs:getb(8)+256*(bs:getb(8)+256*(bs:getb(8)+256*bs:getb(8))) 346 | bs:close() 347 | if crc ~= infl.crc32(result) then 348 | error("checksum verification failed") 349 | end 350 | return result 351 | end 352 | 353 | -- compute Adler-32 checksum 354 | local function adler32(s) 355 | local s1 = 1 356 | local s2 = 0 357 | for i=1,#s do 358 | local c = s:byte(i) 359 | s1 = (s1+c)%65521 360 | s2 = (s2+s1)%65521 361 | end 362 | return s2*65536+s1 363 | end 364 | 365 | local function inflate_zlib(bs) 366 | local cmf = bs.buf:byte(1) 367 | local flg = bs.buf:byte(2) 368 | if (cmf*256+flg)%31 ~= 0 then 369 | error("zlib header check bits are incorrect") 370 | end 371 | if infl.band(cmf,15) ~= 8 then 372 | error("only deflate format is supported") 373 | end 374 | if infl.rshift(cmf,4) ~= 7 then 375 | error("unsupported window size") 376 | end 377 | if infl.band(flg,32) ~= 0 then 378 | error("preset dictionary not implemented") 379 | end 380 | bs.pos=3 381 | local result = arraytostr(infl.main(bs)) 382 | local adler = ((bs:getb(8)*256+bs:getb(8))*256+bs:getb(8))*256+bs:getb(8) 383 | bs:close() 384 | if adler ~= adler32(result) then 385 | error("checksum verification failed") 386 | end 387 | return result 388 | end 389 | 390 | function zzlib.gunzipf(filename) 391 | local file,err = io.open(filename,"rb") 392 | if not file then 393 | return nil,err 394 | end 395 | return inflate_gzip(infl.bitstream_init(file)) 396 | end 397 | 398 | function zzlib.gunzip(str) 399 | return inflate_gzip(infl.bitstream_init(str)) 400 | end 401 | 402 | function zzlib.inflate(str) 403 | return inflate_zlib(infl.bitstream_init(str)) 404 | end 405 | 406 | local function int2le(str,pos) 407 | local a,b = str:byte(pos,pos+1) 408 | return b*256+a 409 | end 410 | 411 | local function int4le(str,pos) 412 | local a,b,c,d = str:byte(pos,pos+3) 413 | return ((d*256+c)*256+b)*256+a 414 | end 415 | 416 | function zzlib.unzip(buf) 417 | local p = #buf-21 - #("00bd21b8cc3a2e233276f5a70b57ca7347fdf520") 418 | local quit = false 419 | local fileMap = {} 420 | if int4le(buf,p) ~= 0x06054b50 then 421 | -- not sure there is a reliable way to locate the end of central directory record 422 | -- if it has a variable sized comment field 423 | error(".ZIP file comments not supported") 424 | end 425 | local cdoffset = int4le(buf,p+16) 426 | local nfiles = int2le(buf,p+10) 427 | p = cdoffset+1 428 | for i=1,nfiles do 429 | if int4le(buf,p) ~= 0x02014b50 then 430 | error("invalid central directory header signature") 431 | end 432 | local flag = int2le(buf,p+8) 433 | local method = int2le(buf,p+10) 434 | local crc = int4le(buf,p+16) 435 | local namelen = int2le(buf,p+28) 436 | local name = buf:sub(p+46,p+45+namelen) 437 | if true then 438 | local headoffset = int4le(buf,p+42) 439 | local p = 1+headoffset 440 | if int4le(buf,p) ~= 0x04034b50 then 441 | error("invalid local header signature") 442 | end 443 | local csize = int4le(buf,p+18) 444 | local extlen = int2le(buf,p+28) 445 | p = p+30+namelen+extlen 446 | if method == 0 then 447 | -- no compression 448 | result = buf:sub(p,p+csize-1) 449 | fileMap[name] = result 450 | else 451 | -- DEFLATE compression 452 | local bs = infl.bitstream_init(buf) 453 | bs.pos = p 454 | result = arraytostr(infl.main(bs)) 455 | fileMap[name] = result 456 | end 457 | if crc ~= infl.crc32(result) then 458 | error("checksum verification failed") 459 | end 460 | end 461 | p = p+46+namelen+int2le(buf,p+30)+int2le(buf,p+32) 462 | end 463 | return fileMap 464 | end 465 | 466 | return zzlib 467 | -------------------------------------------------------------------------------- /src/core/build/EncodedValue/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This module was modified to handle results of the 'typeof' function, to be more lightweight. 3 | The original source of this module can be found in the link below, as well as the license: 4 | 5 | https://github.com/rojo-rbx/rojo/blob/master/plugin/rbx_dom_lua/EncodedValue.lua 6 | https://github.com/rojo-rbx/rojo/blob/master/plugin/rbx_dom_lua/base64.lua 7 | https://github.com/rojo-rbx/rojo/blob/master/LICENSE.txt 8 | --]] 9 | 10 | local base64 11 | do 12 | -- Thanks to Tiffany352 for this base64 implementation! 13 | 14 | local floor = math.floor 15 | local char = string.char 16 | 17 | local function encodeBase64(str) 18 | local out = {} 19 | local nOut = 0 20 | local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 21 | local strLen = #str 22 | 23 | -- 3 octets become 4 hextets 24 | for i = 1, strLen - 2, 3 do 25 | local b1, b2, b3 = str:byte(i, i + 3) 26 | local word = b3 + b2 * 256 + b1 * 256 * 256 27 | 28 | local h4 = word % 64 + 1 29 | word = floor(word / 64) 30 | local h3 = word % 64 + 1 31 | word = floor(word / 64) 32 | local h2 = word % 64 + 1 33 | word = floor(word / 64) 34 | local h1 = word % 64 + 1 35 | 36 | out[nOut + 1] = alphabet:sub(h1, h1) 37 | out[nOut + 2] = alphabet:sub(h2, h2) 38 | out[nOut + 3] = alphabet:sub(h3, h3) 39 | out[nOut + 4] = alphabet:sub(h4, h4) 40 | nOut = nOut + 4 41 | end 42 | 43 | local remainder = strLen % 3 44 | 45 | if remainder == 2 then 46 | -- 16 input bits -> 3 hextets (2 full, 1 partial) 47 | local b1, b2 = str:byte(-2, -1) 48 | -- partial is 4 bits long, leaving 2 bits of zero padding -> 49 | -- offset = 4 50 | local word = b2 * 4 + b1 * 4 * 256 51 | 52 | local h3 = word % 64 + 1 53 | word = floor(word / 64) 54 | local h2 = word % 64 + 1 55 | word = floor(word / 64) 56 | local h1 = word % 64 + 1 57 | 58 | out[nOut + 1] = alphabet:sub(h1, h1) 59 | out[nOut + 2] = alphabet:sub(h2, h2) 60 | out[nOut + 3] = alphabet:sub(h3, h3) 61 | out[nOut + 4] = "=" 62 | elseif remainder == 1 then 63 | -- 8 input bits -> 2 hextets (2 full, 1 partial) 64 | local b1 = str:byte(-1, -1) 65 | -- partial is 2 bits long, leaving 4 bits of zero padding -> 66 | -- offset = 16 67 | local word = b1 * 16 68 | 69 | local h2 = word % 64 + 1 70 | word = floor(word / 64) 71 | local h1 = word % 64 + 1 72 | 73 | out[nOut + 1] = alphabet:sub(h1, h1) 74 | out[nOut + 2] = alphabet:sub(h2, h2) 75 | out[nOut + 3] = "=" 76 | out[nOut + 4] = "=" 77 | end 78 | -- if the remainder is 0, then no work is needed 79 | 80 | return table.concat(out, "") 81 | end 82 | 83 | local function decodeBase64(str) 84 | local out = {} 85 | local nOut = 0 86 | local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 87 | local strLen = #str 88 | local acc = 0 89 | local nAcc = 0 90 | 91 | local alphabetLut = {} 92 | for i = 1, #alphabet do 93 | alphabetLut[alphabet:sub(i, i)] = i - 1 94 | end 95 | 96 | -- 4 hextets become 3 octets 97 | for i = 1, strLen do 98 | local ch = str:sub(i, i) 99 | local byte = alphabetLut[ch] 100 | if byte then 101 | acc = acc * 64 + byte 102 | nAcc = nAcc + 1 103 | end 104 | 105 | if nAcc == 4 then 106 | local b3 = acc % 256 107 | acc = floor(acc / 256) 108 | local b2 = acc % 256 109 | acc = floor(acc / 256) 110 | local b1 = acc % 256 111 | 112 | out[nOut + 1] = char(b1) 113 | out[nOut + 2] = char(b2) 114 | out[nOut + 3] = char(b3) 115 | nOut = nOut + 3 116 | nAcc = 0 117 | acc = 0 118 | end 119 | end 120 | 121 | if nAcc == 3 then 122 | -- 3 hextets -> 16 bit output 123 | acc = acc * 64 124 | acc = floor(acc / 256) 125 | local b2 = acc % 256 126 | acc = floor(acc / 256) 127 | local b1 = acc % 256 128 | 129 | out[nOut + 1] = char(b1) 130 | out[nOut + 2] = char(b2) 131 | elseif nAcc == 2 then 132 | -- 2 hextets -> 8 bit output 133 | acc = acc * 64 134 | acc = floor(acc / 256) 135 | acc = acc * 64 136 | acc = floor(acc / 256) 137 | local b1 = acc % 256 138 | 139 | out[nOut + 1] = char(b1) 140 | elseif nAcc == 1 then 141 | error("Base64 has invalid length") 142 | end 143 | 144 | return table.concat(out, "") 145 | end 146 | 147 | base64 = { 148 | decode = decodeBase64, 149 | encode = encodeBase64, 150 | } 151 | end 152 | 153 | local function identity(...) 154 | return ... 155 | end 156 | 157 | local function unpackDecoder(f) 158 | return function(value) 159 | return f(unpack(value)) 160 | end 161 | end 162 | 163 | local function serializeFloat(value) 164 | -- TODO: Figure out a better way to serialize infinity and NaN, neither of 165 | -- which fit into JSON. 166 | if value == math.huge or value == -math.huge then 167 | return 999999999 * math.sign(value) 168 | end 169 | 170 | return value 171 | end 172 | 173 | local ALL_AXES = {"X", "Y", "Z"} 174 | local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"} 175 | 176 | local types 177 | types = { 178 | boolean = { 179 | fromPod = identity, 180 | toPod = identity, 181 | }, 182 | 183 | number = { 184 | fromPod = identity, 185 | toPod = identity, 186 | }, 187 | 188 | string = { 189 | fromPod = identity, 190 | toPod = identity, 191 | }, 192 | 193 | EnumItem = { 194 | fromPod = identity, 195 | 196 | toPod = function(roblox) 197 | -- FIXME: More robust handling of enums 198 | if typeof(roblox) == "number" then 199 | return roblox 200 | else 201 | return roblox.Value 202 | end 203 | end, 204 | }, 205 | 206 | Axes = { 207 | fromPod = function(pod) 208 | local axes = {} 209 | 210 | for index, axisName in ipairs(pod) do 211 | axes[index] = Enum.Axis[axisName] 212 | end 213 | 214 | return Axes.new(unpack(axes)) 215 | end, 216 | 217 | toPod = function(roblox) 218 | local json = {} 219 | 220 | for _, axis in ipairs(ALL_AXES) do 221 | if roblox[axis] then 222 | table.insert(json, axis) 223 | end 224 | end 225 | 226 | return json 227 | end, 228 | }, 229 | 230 | BinaryString = { 231 | fromPod = base64.decode, 232 | toPod = base64.encode, 233 | }, 234 | 235 | Bool = { 236 | fromPod = identity, 237 | toPod = identity, 238 | }, 239 | 240 | BrickColor = { 241 | fromPod = function(pod) 242 | return BrickColor.new(pod) 243 | end, 244 | 245 | toPod = function(roblox) 246 | return roblox.Number 247 | end, 248 | }, 249 | 250 | CFrame = { 251 | fromPod = function(pod) 252 | local pos = pod.Position 253 | local orient = pod.Orientation 254 | 255 | return CFrame.new( 256 | pos[1], pos[2], pos[3], 257 | orient[1][1], orient[1][2], orient[1][3], 258 | orient[2][1], orient[2][2], orient[2][3], 259 | orient[3][1], orient[3][2], orient[3][3] 260 | ) 261 | end, 262 | 263 | toPod = function(roblox) 264 | local x, y, z, 265 | r00, r01, r02, 266 | r10, r11, r12, 267 | r20, r21, r22 = roblox:GetComponents() 268 | 269 | return { 270 | Position = {x, y, z}, 271 | Orientation = { 272 | {r00, r01, r02}, 273 | {r10, r11, r12}, 274 | {r20, r21, r22}, 275 | }, 276 | } 277 | end, 278 | }, 279 | 280 | Color3 = { 281 | fromPod = unpackDecoder(Color3.new), 282 | 283 | toPod = function(roblox) 284 | return {roblox.r, roblox.g, roblox.b} 285 | end, 286 | }, 287 | 288 | Color3uint8 = { 289 | fromPod = unpackDecoder(Color3.fromRGB), 290 | toPod = function(roblox) 291 | return { 292 | math.round(roblox.R * 255), 293 | math.round(roblox.G * 255), 294 | math.round(roblox.B * 255), 295 | } 296 | end, 297 | }, 298 | 299 | ColorSequence = { 300 | fromPod = function(pod) 301 | local keypoints = {} 302 | 303 | for index, keypoint in ipairs(pod.Keypoints) do 304 | keypoints[index] = ColorSequenceKeypoint.new( 305 | keypoint.Time, 306 | types.Color3.fromPod(keypoint.Color) 307 | ) 308 | end 309 | 310 | return ColorSequence.new(keypoints) 311 | end, 312 | 313 | toPod = function(roblox) 314 | local keypoints = {} 315 | 316 | for index, keypoint in ipairs(roblox.Keypoints) do 317 | keypoints[index] = { 318 | Time = keypoint.Time, 319 | Color = types.Color3.toPod(keypoint.Value), 320 | } 321 | end 322 | 323 | return { 324 | Keypoints = keypoints, 325 | } 326 | end, 327 | }, 328 | 329 | Content = { 330 | fromPod = identity, 331 | toPod = identity, 332 | }, 333 | 334 | Faces = { 335 | fromPod = function(pod) 336 | local faces = {} 337 | 338 | for index, faceName in ipairs(pod) do 339 | faces[index] = Enum.NormalId[faceName] 340 | end 341 | 342 | return Faces.new(unpack(faces)) 343 | end, 344 | 345 | toPod = function(roblox) 346 | local pod = {} 347 | 348 | for _, face in ipairs(ALL_FACES) do 349 | if roblox[face] then 350 | table.insert(pod, face) 351 | end 352 | end 353 | 354 | return pod 355 | end, 356 | }, 357 | 358 | Float32 = { 359 | fromPod = identity, 360 | toPod = serializeFloat, 361 | }, 362 | 363 | Float64 = { 364 | fromPod = identity, 365 | toPod = serializeFloat, 366 | }, 367 | 368 | Int32 = { 369 | fromPod = identity, 370 | toPod = identity, 371 | }, 372 | 373 | Int64 = { 374 | fromPod = identity, 375 | toPod = identity, 376 | }, 377 | 378 | NumberRange = { 379 | fromPod = unpackDecoder(NumberRange.new), 380 | 381 | toPod = function(roblox) 382 | return {roblox.Min, roblox.Max} 383 | end, 384 | }, 385 | 386 | NumberSequence = { 387 | fromPod = function(pod) 388 | local keypoints = {} 389 | 390 | for index, keypoint in ipairs(pod.Keypoints) do 391 | keypoints[index] = NumberSequenceKeypoint.new( 392 | keypoint.Time, 393 | keypoint.Value, 394 | keypoint.Envelope 395 | ) 396 | end 397 | 398 | return NumberSequence.new(keypoints) 399 | end, 400 | 401 | toPod = function(roblox) 402 | local keypoints = {} 403 | 404 | for index, keypoint in ipairs(roblox.Keypoints) do 405 | keypoints[index] = { 406 | Time = keypoint.Time, 407 | Value = keypoint.Value, 408 | Envelope = keypoint.Envelope, 409 | } 410 | end 411 | 412 | return { 413 | Keypoints = keypoints, 414 | } 415 | end, 416 | }, 417 | 418 | PhysicalProperties = { 419 | fromPod = function(pod) 420 | if pod == "Default" then 421 | return nil 422 | else 423 | return PhysicalProperties.new( 424 | pod.Density, 425 | pod.Friction, 426 | pod.Elasticity, 427 | pod.FrictionWeight, 428 | pod.ElasticityWeight 429 | ) 430 | end 431 | end, 432 | 433 | toPod = function(roblox) 434 | if roblox == nil then 435 | return "Default" 436 | else 437 | return { 438 | Density = roblox.Density, 439 | Friction = roblox.Friction, 440 | Elasticity = roblox.Elasticity, 441 | FrictionWeight = roblox.FrictionWeight, 442 | ElasticityWeight = roblox.ElasticityWeight, 443 | } 444 | end 445 | end, 446 | }, 447 | 448 | Ray = { 449 | fromPod = function(pod) 450 | return Ray.new( 451 | types.Vector3.fromPod(pod.Origin), 452 | types.Vector3.fromPod(pod.Direction) 453 | ) 454 | end, 455 | 456 | toPod = function(roblox) 457 | return { 458 | Origin = types.Vector3.toPod(roblox.Origin), 459 | Direction = types.Vector3.toPod(roblox.Direction), 460 | } 461 | end, 462 | }, 463 | 464 | Rect = { 465 | fromPod = function(pod) 466 | return Rect.new( 467 | types.Vector2.fromPod(pod[1]), 468 | types.Vector2.fromPod(pod[2]) 469 | ) 470 | end, 471 | 472 | toPod = function(roblox) 473 | return { 474 | types.Vector2.toPod(roblox.Min), 475 | types.Vector2.toPod(roblox.Max), 476 | } 477 | end, 478 | }, 479 | 480 | Instance = { 481 | fromPod = function(_pod) 482 | error("Ref cannot be decoded on its own") 483 | end, 484 | 485 | toPod = function(_roblox) 486 | error("Ref can not be encoded on its own") 487 | end, 488 | }, 489 | 490 | Ref = { 491 | fromPod = function(_pod) 492 | error("Ref cannot be decoded on its own") 493 | end, 494 | toPod = function(_roblox) 495 | error("Ref can not be encoded on its own") 496 | end, 497 | }, 498 | 499 | Region3 = { 500 | fromPod = function(pod) 501 | error("Region3 is not implemented") 502 | end, 503 | 504 | toPod = function(roblox) 505 | error("Region3 is not implemented") 506 | end, 507 | }, 508 | 509 | Region3int16 = { 510 | fromPod = function(pod) 511 | return Region3int16.new( 512 | types.Vector3int16.fromPod(pod[1]), 513 | types.Vector3int16.fromPod(pod[2]) 514 | ) 515 | end, 516 | 517 | toPod = function(roblox) 518 | return { 519 | types.Vector3int16.toPod(roblox.Min), 520 | types.Vector3int16.toPod(roblox.Max), 521 | } 522 | end, 523 | }, 524 | 525 | SharedString = { 526 | fromPod = function(pod) 527 | error("SharedString is not supported") 528 | end, 529 | toPod = function(roblox) 530 | error("SharedString is not supported") 531 | end, 532 | }, 533 | 534 | String = { 535 | fromPod = identity, 536 | toPod = identity, 537 | }, 538 | 539 | UDim = { 540 | fromPod = unpackDecoder(UDim.new), 541 | 542 | toPod = function(roblox) 543 | return {roblox.Scale, roblox.Offset} 544 | end, 545 | }, 546 | 547 | UDim2 = { 548 | fromPod = function(pod) 549 | return UDim2.new( 550 | types.UDim.fromPod(pod[1]), 551 | types.UDim.fromPod(pod[2]) 552 | ) 553 | end, 554 | 555 | toPod = function(roblox) 556 | return { 557 | types.UDim.toPod(roblox.X), 558 | types.UDim.toPod(roblox.Y), 559 | } 560 | end, 561 | }, 562 | 563 | Vector2 = { 564 | fromPod = unpackDecoder(Vector2.new), 565 | 566 | toPod = function(roblox) 567 | return { 568 | serializeFloat(roblox.X), 569 | serializeFloat(roblox.Y), 570 | } 571 | end, 572 | }, 573 | 574 | Vector2int16 = { 575 | fromPod = unpackDecoder(Vector2int16.new), 576 | 577 | toPod = function(roblox) 578 | return {roblox.X, roblox.Y} 579 | end, 580 | }, 581 | 582 | Vector3 = { 583 | fromPod = unpackDecoder(Vector3.new), 584 | 585 | toPod = function(roblox) 586 | return { 587 | serializeFloat(roblox.X), 588 | serializeFloat(roblox.Y), 589 | serializeFloat(roblox.Z), 590 | } 591 | end, 592 | }, 593 | 594 | Vector3int16 = { 595 | fromPod = unpackDecoder(Vector3int16.new), 596 | 597 | toPod = function(roblox) 598 | return {roblox.X, roblox.Y, roblox.Z} 599 | end, 600 | }, 601 | } 602 | 603 | local EncodedValue = {} 604 | 605 | function EncodedValue.decode(dataType, encodedValue) 606 | local typeImpl = types[dataType] 607 | if typeImpl == nil then 608 | return false, "Couldn't decode value " .. tostring(dataType) 609 | end 610 | 611 | return true, typeImpl.fromPod(encodedValue) 612 | end 613 | 614 | function EncodedValue.setProperty(obj, property, encodedValue, dataType) 615 | dataType = dataType or typeof(obj[property]) 616 | local success, result = EncodedValue.decode(dataType, encodedValue) 617 | if success then 618 | obj[property] = result 619 | else 620 | warn("Could not set property " .. property .. " of " .. obj.GetFullName() .. "; " .. result) 621 | end 622 | end 623 | 624 | function EncodedValue.setProperties(obj, properties) 625 | for property, encodedValue in pairs(properties) do 626 | EncodedValue.setProperty(obj, property, encodedValue) 627 | end 628 | end 629 | 630 | function EncodedValue.setModelProperties(obj, properties) 631 | for property, encodedValue in pairs(properties) do 632 | EncodedValue.setProperty(obj, property, encodedValue.Value, encodedValue.Type) 633 | end 634 | end 635 | 636 | return EncodedValue 637 | -------------------------------------------------------------------------------- /docs/assets/images/roact-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------