├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── default.project.json ├── foreman.toml ├── moonwave.toml ├── rojo-packaged.project.json ├── rojo-test.project.json ├── selene.toml ├── src ├── lib │ ├── Select.lua │ └── init.lua └── packaged │ └── init.lua ├── test ├── stories │ ├── assets.rbxm │ ├── changes.story │ │ └── init.lua │ ├── samename.story │ │ └── init.lua │ ├── selectors.story │ │ └── init.lua │ ├── simple.story │ │ └── init.lua │ └── wrapped.story │ │ └── init.lua ├── wally.lock └── wally.toml ├── wally.lock └── wally.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | 7 | [*.json] 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.lua] 12 | indent_style = tab 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Check out Repository 8 | uses: actions/checkout@v2 9 | 10 | - name: Install Foreman 11 | uses: Roblox/setup-foreman@v1 12 | with: 13 | version: "^1.0.0" 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | 16 | - name: Pull Packages (Manually) 17 | run: | 18 | # We do it this way so that it's easier for non-wally users to update the static API dump 19 | mkdir Packages; 20 | git clone --depth 1 --branch latest-v1 --single-branch https://github.com/Corecii/api-dump-static ./Packages/ApiDumpStatic 21 | 22 | - name: Build Packaged Library 23 | run: rojo build -o RoactTemplate.rbxm rojo-packaged.project.json 24 | 25 | - name: Archive Packaged Library 26 | uses: actions/upload-artifact@v2 27 | with: 28 | name: Library (packaged with dependencies) 29 | path: RoactTemplate.rbxm -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check out Repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Install Foreman 16 | uses: Roblox/setup-foreman@v1 17 | with: 18 | version: "^1.0.0" 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Pull Packages (Manually) 22 | run: | 23 | # We do it this way so that it's easier for non-wally users to update the static API dump 24 | mkdir Packages; 25 | git clone --depth 1 --branch latest-v1 --single-branch https://github.com/Corecii/api-dump-static ./Packages/ApiDumpStatic 26 | cd 27 | 28 | - name: Build Packaged Library 29 | run: rojo build -o RoactTemplate.rbxm rojo-packaged.project.json 30 | 31 | - name: Archive Packaged Library 32 | uses: actions/upload-artifact@v2 33 | with: 34 | name: RoactTemplatePackaged 35 | path: RoactTemplate.rbxm 36 | 37 | publish: 38 | name: Upload release artifacts 39 | runs-on: ubuntu-latest 40 | needs: ['build'] 41 | steps: 42 | - uses: actions/checkout@v2 43 | 44 | - uses: actions/download-artifact@v2 45 | 46 | - name: List directory and files 47 | run: | 48 | pwd 49 | ls -l 50 | 51 | - name: Get tag name 52 | run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 53 | 54 | - name: List tag and files 55 | run: | 56 | echo ${{ env.TAG }} 57 | ls -l 58 | 59 | - name: Upload release artifacts 60 | run: gh release upload ${{ env.TAG }} RoactTemplatePackaged/RoactTemplate.rbxm 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .foreman-install/ 3 | 4 | roblox.toml 5 | 6 | /*.rbxlx 7 | /*.rbxm 8 | 9 | /build/ 10 | 11 | /node_modules 12 | 13 | /Packages 14 | /test/Packages 15 | 16 | api-dump-split.remodel.json 17 | previous-package-version.txt 18 | no-changes 19 | globalTypes.d.lua -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This document lists changes for each release. 4 | 5 | ## Upcoming 6 | 7 | ## v0.2.0 8 | 9 | * Children elements are now created after computing changed props from 10 | selectors. This is more efficient in cases where children will be removed or 11 | computed separately. For example, deleting children in Changes or wrapping a 12 | template in a component. 13 | * Children with the same name are now wrapped in a fragment with that name. 14 | This allows you to remove/replace all children with a given name. 15 | * Changed signature of ChangesCallback. It now takes a template instance and 16 | returns a set of changes. 17 | * The old signature provided `props` and `children`. Props was useless since 18 | only non-default props were defined. Children were useless since they were 19 | Roact elements not meant for reading. 20 | * The new signature provides the instance being used for the template. This 21 | allows you to access the props and children of the instance. You now return 22 | a list of changes instead of mutating the props and children. 23 | * Some error messages are now more clear and provide direction on the correct 24 | way to do things. 25 | * There are more typechecks on the user-facing part of the library. 26 | 27 | ## v0.1.0 28 | 29 | Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Shae A. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roact Template (from Instance) 2 | 3 | A library that allows you to load an instance as a Roact component at runtime 4 | 5 | [Documentation](https://corecii.github.io/roact-template/) 6 | 7 | Install with [wally](https://wally.run): 8 | ```toml 9 | # wally.toml 10 | [dependencies] 11 | RoactTemplate = "corecii/roact-template@0.2.0" 12 | ``` 13 | 14 | When API dumps become outdated, open your `wally.lock` and remove the `corecii/api-dump-static` section, then run `wally install` again. 15 | *(in the future, you will be able to use `wally upgrade`, but upgrade is not available yet)* 16 | 17 | You can also use the [packaged release model](https://github.com/Corecii/roact-template/releases/latest), 18 | but be aware that the internal 19 | API dump is *not* updated with every Roblox update. When using Wally, the newest API 20 | dump is fetched automatically on install. When using the pre-packaged model, the 21 | latest API dump is *not* fetched automatically. You can download the latest API 22 | dump [here](https://github.com/Corecii/api-dump-static/releases/latest) 23 | to replace the included one with the newest version. 24 | 25 | --- 26 | 27 | RoactTemplate allows you to change descendants of your template interface. You can: 28 | * Change descendants by their name 29 | * Assign descendants props and children 30 | * Wrap descendants in components (see `wrapped` in the docs!) 31 | * Select descendants to change according to a custom callback or pre-made selectors 32 | 33 | ### Quick Simple Example 34 | 35 | ```lua 36 | local Roact = require(game.ReplicatedStorage.Packages.Roact) 37 | local RoactTemplate = require(game.ReplicatedStorage.Packages.RoactTemplate) 38 | 39 | local InventoryTemplate = RoactTemplate.fromInstance(Roact, UITemplates.InventoryApp) 40 | 41 | local function InventoryApp(props) 42 | return Roact.createElement(InventoryTemplate, { 43 | WindowTitle = { 44 | Text = props.category, 45 | }, 46 | OuterFrame = { 47 | Visible = props.visible, 48 | }, 49 | Scroller = { 50 | [Roact.Children] = makeInventoryItems(props.items), 51 | }, 52 | }) 53 | end 54 | ``` 55 | 56 | --- 57 | 58 | ## Notes 59 | 60 | Compared to the "convert to Roact *code*" approach, this allows you to easily 61 | edit UI in Studio even after you start programming it. 62 | 63 | RoactTemplate only replaces the *static* parts of your UI. This allows you to 64 | program the dynamic parts declaratively, while using Roblox's great built-in 65 | editor for the static parts. 66 | 67 | Roact is based on React, which was made for web development. Web development is 68 | founded on hand-writing structure (HTML) and style (CSS) markup. Roblox, on the 69 | other hand, is founded on a great editor, and lacks a CSS-equivalent. Because of 70 | these differences, using Roact typically involves throwing out one of Roblox's 71 | biggest advantages: its editor. RoactTemplate allows you to use the editor as 72 | you would before Roact existed, then program your UI declaratively using Roact. 73 | It's the best of both worlds! 74 | 75 | I recommend storing your UI templates as `rbxm` or `rbxmx` files next to or as a 76 | child of the code that uses the UI templates. You should ideally store each 77 | interface or component as its own model so that resolving merge conflicts is easier. 78 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoactTemplate", 3 | "tree": { 4 | "$path": "src/lib" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rojo = { source = "rojo-rbx/rojo", version = "7.0.0" } 3 | wally = { source = "UpliftGames/wally", version = "0.3.1" } 4 | luau-analyze = { source = "JohnnyMorganz/luau-analyze-rojo", version = "0.525.2" } -------------------------------------------------------------------------------- /moonwave.toml: -------------------------------------------------------------------------------- 1 | title = "RoactTemplate" 2 | 3 | gitSourceBranch = "main" 4 | 5 | [docusaurus] 6 | organizationName = "Shae A" 7 | projectName = "roact-template" 8 | url = "https://corecii.github.io" 9 | baseUrl = "/roact-template/" 10 | tagline = "A runtime Instance-to-Roact translator" -------------------------------------------------------------------------------- /rojo-packaged.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoactTemplate", 3 | "tree": { 4 | "$path": "src/packaged", 5 | "packages": { 6 | "$path": "Packages", 7 | "lib": { 8 | "$path": "src/lib" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rojo-test.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "lib": { 8 | "$path": "src/packaged", 9 | "packages": { 10 | "$path": "test/Packages", 11 | "lib": { 12 | "$path": "src/lib" 13 | }, 14 | "stories": { 15 | "$path": "test/stories" 16 | } 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" -------------------------------------------------------------------------------- /src/lib/Select.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | local CollectionService = game:GetService("CollectionService") 3 | 4 | local ApiDumpStatic = require(script.Parent.Parent.ApiDumpStatic) 5 | 6 | local function newSelector(call: (instance: Instance, T...) -> boolean): (T...) -> (instance: Instance) -> boolean 7 | return function(...) 8 | local args = table.pack(...) 9 | return function(instance: Instance) 10 | return call(instance, unpack(args, 1, args.n)) 11 | end 12 | end 13 | end 14 | 15 | --[=[ 16 | @class Select 17 | 18 | Selectors to use to change descendent elements. 19 | 20 | These selectors **will be slower** than name-based selection. Name selection 21 | is preferred since we can do fast look-ups to check if any descendant has a 22 | given name. 23 | ]=] 24 | 25 | local Select = {} 26 | 27 | --[=[ 28 | @function prop 29 | @within Select 30 | @param prop string 31 | @param value any 32 | @return (instance: Instance) -> boolean 33 | 34 | Selects an element according to the value of a property 35 | ]=] 36 | Select.prop = newSelector(function(instance, prop, value) 37 | local info = ApiDumpStatic.Classes[instance.ClassName]:Properties()[prop] 38 | if not info then 39 | return false 40 | end 41 | if 42 | not (info.Security == "None" or info.Security.Read == "None") 43 | or (info.Tags and table.find(info.Tags, "NotScriptable")) 44 | then 45 | return false 46 | end 47 | 48 | return (instance :: any)[prop] == value 49 | end) 50 | 51 | --[=[ 52 | Selects an element according to its name. 53 | 54 | This is *slower* than the default `["name"] = { changes }` syntax. 55 | ]=] 56 | function Select.name(name: string): (instance: Instance) -> boolean 57 | return Select.prop("Name", name) 58 | end 59 | 60 | --[=[ 61 | Selects an element according to its class name. 62 | ]=] 63 | function Select.class(class: string): (instance: Instance) -> boolean 64 | return Select.prop("ClassName", class) 65 | end 66 | 67 | --[=[ 68 | @function isA 69 | @within Select 70 | @param prop string 71 | @param value any 72 | @return (instance: Instance) -> boolean 73 | 74 | Selects an element according to if it `:IsA` class 75 | ]=] 76 | Select.isA = newSelector(function(instance, class) 77 | return instance:IsA(class) 78 | end) 79 | 80 | --[=[ 81 | @function propPattern 82 | @within Select 83 | @param prop string 84 | @param pattern string 85 | @return (instance: Instance) -> boolean 86 | 87 | Selects an element according whether the value of a property matches a pattern. 88 | ]=] 89 | Select.propPattern = newSelector(function(instance, prop, pattern) 90 | local info = ApiDumpStatic.Classes[instance.ClassName]:Properties()[prop] 91 | if not info then 92 | return false 93 | end 94 | if 95 | not (info.Security == "None" or info.Security.Read == "None") 96 | or (info.Tags and table.find(info.Tags, "NotScriptable")) 97 | then 98 | return false 99 | end 100 | 101 | return tostring((instance :: any)[prop]):match(pattern) and true or false 102 | end) 103 | 104 | --[=[ 105 | Selects an element according whether the name matches a pattern. 106 | ]=] 107 | function Select.namePattern(pattern: string): (instance: Instance) -> boolean 108 | return Select.propPattern("Name", pattern) 109 | end 110 | 111 | --[=[ 112 | Selects an element according whether the class name matches a pattern. 113 | 114 | For example, `^Text.+$` matches both `TextLabel` and `TextBox`. 115 | ]=] 116 | function Select.classPattern(pattern: string): (instance: Instance) -> boolean 117 | return Select.propPattern("ClassName", pattern) 118 | end 119 | 120 | --[=[ 121 | @function attribute 122 | @within Select 123 | @param attribute string 124 | @param value string 125 | @return (instance: Instance) -> boolean 126 | 127 | Selects an element according whether the value of an attribute equals a value. 128 | ]=] 129 | Select.attribute = newSelector(function(instance, name, value) 130 | return instance:GetAttribute(name) == value 131 | end) 132 | 133 | --[=[ 134 | @function tag 135 | @within Select 136 | @param tag string 137 | @return (instance: Instance) -> boolean 138 | 139 | Selects an element according whether its template instance has a tag. 140 | ]=] 141 | Select.tag = newSelector(function(instance, tag) 142 | return CollectionService:HasTag(instance, tag) 143 | end) 144 | 145 | -- We intentionally use pairs for the following so that if an array with holes 146 | -- is passed it cannot cause buggy behavior. 147 | 148 | --[=[ 149 | @function some 150 | @within Select 151 | @param ... Selector 152 | @return (instance: Instance) -> boolean 153 | 154 | Selects an element according whether at least one of the given selectors matches. 155 | ]=] 156 | Select.some = newSelector(function(instance, ...: (instance: Instance) -> boolean) 157 | for _, selector in pairs({ ... }) do 158 | if selector(instance) then 159 | return true 160 | end 161 | end 162 | 163 | return false 164 | end) 165 | 166 | --[=[ 167 | @function every 168 | @within Select 169 | @param ... Selector 170 | @return (instance: Instance) -> boolean 171 | 172 | Selects an element according whether all of the given selectors matches. 173 | ]=] 174 | Select.every = newSelector(function(instance, ...: (instance: Instance) -> boolean) 175 | for _, selector in pairs({ ... }) do 176 | if not selector(instance) then 177 | return false 178 | end 179 | end 180 | 181 | return true 182 | end) 183 | 184 | --[=[ 185 | @function no 186 | @within Select 187 | @param selector Selector 188 | @return (instance: Instance) -> boolean 189 | 190 | Selects an element if the given selector fails for the element. 191 | ]=] 192 | Select.no = newSelector(function(instance, selector) 193 | return not selector(instance) 194 | end) 195 | 196 | return Select 197 | -------------------------------------------------------------------------------- /src/lib/init.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | local HttpService = game:GetService("HttpService") 3 | 4 | local ApiDumpStatic = require(script.Parent.ApiDumpStatic) 5 | 6 | local DISALLOWED_PROPS_LIST = { "Parent", "Transparency", "Name", "ClassName" } 7 | 8 | local DISALLOWED_PROPS = {} 9 | for _, value in ipairs(DISALLOWED_PROPS_LIST) do 10 | DISALLOWED_PROPS[value] = true 11 | end 12 | 13 | type Roact = { 14 | createElement: (...any) -> ...any, 15 | createFragment: (...any) -> ...any, 16 | Children: any, 17 | } 18 | 19 | type Element = any 20 | type Component = any 21 | 22 | export type ChangesCallback = (instance: Instance) -> { [any]: any } 23 | export type ChangesTable = { [any]: any } 24 | export type Changes = ChangesCallback | ChangesTable 25 | 26 | export type SelectorCallback = (instance: Instance) -> boolean 27 | export type Selector = string | SelectorCallback | Root 28 | 29 | export type TemplateItemElement = { 30 | type: "element", 31 | singleFragment: boolean?, 32 | isRoot: boolean?, 33 | class: string, 34 | instance: Instance, 35 | props: { [string]: any }, 36 | children: { [string]: TemplateItem }, 37 | } 38 | 39 | export type TemplateItemFragment = { 40 | type: "fragment", 41 | children: { [string]: TemplateItem }, 42 | } 43 | 44 | export type TemplateItem = TemplateItemElement | TemplateItemFragment 45 | 46 | --[=[ 47 | @class RoactTemplate 48 | 49 | A module to create a Roact tree at runtime given an Instance, with the 50 | ability to change descendants using selectors. 51 | 52 | :::info 53 | 54 | The items marked **"Private"** are *not* private, they're just intentionally 55 | hidden by default. If you have a use for them, feel free to use them! 56 | 57 | ::: 58 | ]=] 59 | local RoactTemplate = {} 60 | 61 | --[=[ 62 | @prop Select Select 63 | @within RoactTemplate 64 | 65 | Selectors to use to change descendent elements. 66 | 67 | These selectors **will be slower** than name-based selection. Name selection 68 | is preferred since we can do fast look-ups to check if any child has a given 69 | name. 70 | ]=] 71 | RoactTemplate.Select = require(script.Select) 72 | 73 | --[=[ 74 | @type Selector string | Root | (instance: Instance) -> boolean 75 | @within RoactTemplate 76 | 77 | Used to determine whether a change should apply to an instance. You can 78 | write your own or use the ones under `Select`. 79 | 80 | Name-based selection is preferred over using Selector callbacks because 81 | name-based selection is faster. 82 | ]=] 83 | 84 | --[=[ 85 | @type Changes ChangesTable | ChangesCallback 86 | @within RoactTemplate 87 | ]=] 88 | 89 | --[=[ 90 | @type ChangesTable { [any]: any } 91 | @within RoactTemplate 92 | 93 | Represents properties to overwrite on an element. 94 | 95 | * You can use `RoactTemplate.None` to set a property to nil. 96 | * You can use `[Roact.Children] = {}` to add children to an element. 97 | * If you want to remove a child you need to set it to `RoactTemplate.None`. 98 | * Children with the same name are placed in a fragment with that name. 99 | You can remove them all at once -- or overwrite them all at once -- by 100 | setting the child to None or some element. 101 | ]=] 102 | 103 | --[=[ 104 | @type ChangesCallback (templateInstance: Instance) -> ChangesTable 105 | @within RoactTemplate 106 | 107 | A function that is called on a template instance and returns changes. This 108 | allows you to make changes dependent upon the instance's existing 109 | properties, attributes, etc. For example, you can use this to darken the 110 | color that was set in the studio editor. 111 | 112 | For example: 113 | ```lua 114 | local ButtonTemplate = RoactTemplate.fromInstance(SomeButtonInstance) 115 | 116 | local function Button() 117 | return Roact.createElement(ButtonTemplate, { 118 | Button = function(templateInstance) 119 | if mouseIsOverInstance() then 120 | -- Darken the button if the mouse is over it! 121 | return { 122 | BackgroundColor3 = Color3.new( 123 | templateInstance.BackgroundColor3.R * 0.8, 124 | templateInstance.BackgroundColor3.G * 0.8, 125 | templateInstance.BackgroundColor3.B * 0.8 126 | ), 127 | } 128 | else 129 | return {} 130 | end 131 | end, 132 | }) 133 | end 134 | ``` 135 | 136 | :::caution 137 | 138 | You should not modify `templateInstance` or its children. It won't directly affect 139 | the render, since `instance` is just a template, but it may affect future 140 | renders that depend on the instance. 141 | 142 | ::: 143 | ]=] 144 | 145 | --[=[ 146 | @prop None None 147 | @within RoactTemplate 148 | 149 | Represents "nil" when overwriting props or children. 150 | ]=] 151 | RoactTemplate.None = newproxy(true) 152 | getmetatable(RoactTemplate.None).__tostring = function() 153 | return "" 154 | end 155 | 156 | export type None = typeof(RoactTemplate.None) 157 | 158 | local Wrap = newproxy(true) 159 | getmetatable(Wrap).__tostring = function() 160 | return "" 161 | end 162 | 163 | --[=[ 164 | @prop Root Root 165 | @within RoactTemplate 166 | 167 | A special selector representing the root instance of a template. 168 | 169 | This is an easy alternative to specifying the root instance's name, which 170 | may not stay consistent. 171 | 172 | For example: 173 | ```lua 174 | local function ButtonFromTemplate(instance) 175 | -- instance can be any Button object 176 | local Button = RoactTemplate.fromInstance(instance) 177 | return function(props) 178 | return Roact.createElement(Button, { 179 | -- we don't know the name of the button, so we can use Root instead. 180 | [RoactTemplate.Root] = { 181 | [Roact.Event.Activated] = props.onActivated, 182 | }, 183 | }) 184 | end 185 | end 186 | ``` 187 | ]=] 188 | RoactTemplate.Root = newproxy(true) 189 | getmetatable(RoactTemplate.Root).__tostring = function() 190 | return "" 191 | end 192 | 193 | export type Root = typeof(RoactTemplate.Root) 194 | 195 | --[=[ 196 | Creates a component given an instance. 197 | 198 | The component's props are a dictionary of `Selector -> Changes` where: 199 | * A `Selector` can be a `string` (fast) *or* a callback that acts on the 200 | Instance and returns true/false (slow). 201 | * `Changes` can be a table of properties or a function returning a table of 202 | properties. 203 | 204 | For example: 205 | ```lua 206 | local Roact = require(game.ReplicatedStorage.Packages.Roact) 207 | local RoactTemplate = require(game.ReplicatedStorage.Packages.RoactTemplate) 208 | 209 | local InventoryTemplate = RoactTemplate.fromInstance(Roact, UITemplates.InventoryApp) 210 | 211 | local function InventoryApp(props) 212 | return Roact.createElement(InventoryTemplate, { 213 | WindowTitle = { 214 | Text = props.category, 215 | }, 216 | OuterFrame = { 217 | Visible = props.visible, 218 | }, 219 | Scroller = { 220 | [Roact.Children] = makeInventoryItems(props.items), 221 | }, 222 | }) 223 | end 224 | ``` 225 | ]=] 226 | function RoactTemplate.fromInstance(Roact: Roact, instance: Instance): ({ [Selector]: Changes }) -> Element 227 | return RoactTemplate.componentFromInstance(Roact, instance) 228 | end 229 | 230 | --[=[ 231 | Used to wrap an element with a component. 232 | 233 | Wrapping a descendant element with a component allows you to efficiently 234 | apply stateful changes without re-rendering the whole tree. 235 | 236 | The only provided prop is `template` which can be used to render the 237 | elements we're wrapping. `template` supports the `{ [Selector]: Changes }` 238 | props as usual. 239 | 240 | For example: 241 | ```lua 242 | local TitleComponent = Roact.Component:extend("TitleComponent") 243 | 244 | function TitleComponent:init() 245 | self:setState({ transparency = 1 }) 246 | end 247 | 248 | function TitleComponent:didMount() 249 | self.updater = RunService.Heartbeat:Connect(function() 250 | self:setState({ transparency = math.sin(os.clock() / math.pi) }) 251 | end) 252 | end 253 | 254 | function TitleComponent:render() 255 | return Roact.createElement(self.props.template, { 256 | Title = { 257 | TextTransparency = self.state.transparency, 258 | }, 259 | }) 260 | end 261 | 262 | local InventoryTemplate = RoactTemplate.fromInstance(Roact, ExampleTemplateUI) 263 | 264 | -- Now we can wrap the Title with our TitleComponent to get stateful 265 | -- changes without re-rendering the whole outer component. 266 | Roact.createElement(Template, { 267 | Title = RoactTemplate.wrapped(TitleComponent) 268 | }) 269 | ``` 270 | 271 | :::caution 272 | 273 | The `template` component does not automatically preserve the props passed to 274 | the outer component. If you need the same props, pass similar props when you 275 | createElement your `template` in the component's `render`. 276 | 277 | ***Be careful not to pass the `wrapped` component into the `template`'s 278 | props*** as that would apply the wrapped component recursively, infinitely. 279 | 280 | ::: 281 | ]=] 282 | function RoactTemplate.wrapped(component: Component): Changes 283 | return { 284 | [Wrap] = component, 285 | } 286 | end 287 | 288 | --[=[ 289 | @tag Extra 290 | @private 291 | Creates a component given an instance. This is the same as `fromInstance`. 292 | 293 | See `fromInstance` for more info. 294 | ]=] 295 | function RoactTemplate.componentFromInstance(Roact: Roact, instance: Instance): ({ [Selector]: Changes }) -> Element 296 | if typeof(instance) ~= "Instance" then 297 | warn("instance object related to the following error message:", instance) -- allows user inspection 298 | error("Expected an Instance for instance argument, got " .. typeof(instance)) 299 | end 300 | 301 | local template = RoactTemplate.templateFromInstance(instance) 302 | 303 | return RoactTemplate.componentFromTemplate(Roact, template) 304 | end 305 | 306 | --[=[ 307 | @tag Extra 308 | @private 309 | Creates a static component given an instance. 310 | 311 | This component cannot change since it caches its elements. This is a very 312 | speedy way to show static UI since the Roact element tree is created only 313 | once. 314 | ]=] 315 | function RoactTemplate.staticComponentFromInstance( 316 | Roact: Roact, 317 | instance: Instance, 318 | selectors: { [Selector]: Changes } 319 | ): (...any) -> Element 320 | local template = RoactTemplate.templateFromInstance(instance) 321 | 322 | local element = RoactTemplate.elementFromTemplate(Roact, template, selectors) 323 | 324 | return function() 325 | return element 326 | end 327 | end 328 | 329 | --[=[ 330 | @tag Extra 331 | @private 332 | Returns the `props` necessary to create an instance. The props can be passed 333 | in to `createElement` to create an equivalent instance, minus its children. 334 | ]=] 335 | function RoactTemplate.propertiesFromInstance(instance: Instance): { [string]: any } 336 | assert(typeof(instance) == "Instance", "Expected argument #1 (instance) to be an instance") 337 | 338 | local api = ApiDumpStatic.Classes[instance.ClassName] 339 | assert(api, "Unknown instance type '" .. instance.ClassName .. "'") 340 | 341 | local properties = {} 342 | 343 | for name, info in pairs(api:Properties()) do 344 | if 345 | not DISALLOWED_PROPS[name] 346 | and (info.Security == "None" or (info.Security.Read == "None" and info.Security.Write == "None")) 347 | and not (info.Tags and (table.find(info.Tags, "NotScriptable") or table.find(info.Tags, "ReadOnly"))) 348 | then 349 | pcall(function() 350 | local prop = (instance :: any)[name] 351 | if prop ~= api:GetPropertyDefault(name) then 352 | properties[name] = prop 353 | end 354 | end) 355 | end 356 | end 357 | 358 | return properties 359 | end 360 | 361 | --[=[ 362 | @tag Extra 363 | @private 364 | Returns the internal template for a given instance. 365 | ]=] 366 | function RoactTemplate.templateFromInstance(instance: Instance): TemplateItem 367 | local elements: { [string]: TemplateItem } = {} 368 | 369 | local toProcess = { { instance = instance, parent = elements } } 370 | while #toProcess > 0 do 371 | local item = table.remove(toProcess) 372 | assert(item, "always") -- typechecker assert 373 | 374 | local children = {} 375 | 376 | local props = RoactTemplate.propertiesFromInstance(item.instance) 377 | table.freeze(props) 378 | 379 | local existingItem = item.parent[item.instance.Name] 380 | if existingItem then 381 | if existingItem.type ~= "fragment" then 382 | item.parent[item.instance.Name] = table.freeze({ 383 | type = "fragment" :: "fragment", 384 | children = { 385 | [item.instance.Name] = existingItem, 386 | }, 387 | }) 388 | 389 | existingItem = item.parent[item.instance.Name] 390 | end 391 | 392 | existingItem.children[item.instance.Name .. " " .. HttpService:GenerateGUID()] = table.freeze({ 393 | type = "element" :: "element", 394 | singleFragment = true, 395 | class = item.instance.ClassName, 396 | instance = item.instance, 397 | props = props, 398 | children = children, 399 | }) 400 | else 401 | item.parent[item.instance.Name] = table.freeze({ 402 | type = "element" :: "element", 403 | isRoot = item.instance == instance, 404 | class = item.instance.ClassName, 405 | instance = item.instance, 406 | props = props, 407 | children = children, 408 | }) 409 | end 410 | 411 | for _, child in ipairs(item.instance:GetChildren()) do 412 | table.insert(toProcess, { instance = child, parent = children }) 413 | end 414 | end 415 | 416 | local rootElement = elements[instance.Name] 417 | 418 | return rootElement 419 | end 420 | 421 | local function merge(into: { [any]: any }, from: { [any]: any }, none: any?) 422 | for key, value in pairs(from) do 423 | if value == none then 424 | value = nil 425 | end 426 | into[key] = value 427 | end 428 | end 429 | 430 | local function applySelectors( 431 | Roact: Roact, 432 | template: TemplateItemElement, 433 | slowSelectors: { [SelectorCallback]: Changes }?, 434 | fastSelectors: { [string | Root]: Changes }? 435 | ): ({ [string]: any }, { [string]: any }) 436 | if not (fastSelectors or slowSelectors) then 437 | return table.clone(template.props), {} 438 | end 439 | 440 | local newProps = {} 441 | local newChildren = {} 442 | 443 | local function apply(changes: Changes) 444 | if typeof(changes) == "function" then 445 | local changesResult = changes(template.instance) 446 | 447 | if typeof(changesResult) ~= "table" then 448 | local sourceId, line, name = debug.info(changes, "sln") 449 | warn("Changes object related to the following error message:", changes) -- allows user inspection 450 | error( 451 | string.format( 452 | "Expected a table from ChangesCallback, got %s. Callback: %s:%s: %s", 453 | typeof(changesResult), 454 | tostring(sourceId or "[unknown]"), 455 | tostring(line or "unknown"), 456 | tostring(name or "unknown") 457 | ) 458 | ) 459 | end 460 | 461 | changes = changesResult 462 | end 463 | 464 | if typeof(changes) ~= "table" then 465 | warn("Changes object related to the following error message:", changes) -- allows user inspection 466 | error("Expected a table or function for Changes, got " .. typeof(changes)) 467 | end 468 | 469 | assert(typeof(changes) == "table", "always") -- typechecker assert 470 | 471 | merge(newProps, changes) 472 | 473 | if newProps[Roact.Children] then 474 | merge(newChildren, newProps[Roact.Children]) 475 | newProps[Roact.Children] = nil 476 | end 477 | end 478 | 479 | if fastSelectors then 480 | if fastSelectors[RoactTemplate.Root] and template.isRoot then 481 | apply(fastSelectors[RoactTemplate.Root]) 482 | end 483 | 484 | if fastSelectors[template.instance.Name] then 485 | apply(fastSelectors[template.instance.Name]) 486 | end 487 | end 488 | 489 | if slowSelectors then 490 | for selector, changes in pairs(slowSelectors) do 491 | if selector(template.instance) then 492 | apply(changes) 493 | end 494 | end 495 | end 496 | 497 | return newProps, newChildren 498 | end 499 | 500 | local function elementFromTemplate( 501 | Roact: Roact, 502 | template: TemplateItem, 503 | slowSelectors: { [SelectorCallback]: any }?, 504 | fastSelectors: { [string | Root]: any }? 505 | ): any 506 | if template.type == "fragment" then 507 | local children = {} 508 | 509 | for name, child in pairs(template.children) do 510 | children[name] = elementFromTemplate(Roact, child, slowSelectors, fastSelectors) 511 | end 512 | 513 | return Roact.createFragment(children) 514 | end 515 | assert(template.type == "element", "always") -- typechecker assert 516 | 517 | local newProps, newChildren = applySelectors(Roact, template, slowSelectors, fastSelectors) 518 | 519 | local element 520 | if newProps[Wrap] then 521 | element = Roact.createElement(newProps[Wrap], { 522 | template = RoactTemplate.componentFromTemplate(Roact, template), 523 | }) 524 | else 525 | local props = table.clone(template.props) 526 | merge(props, newProps) 527 | 528 | local children = {} 529 | for name, child in pairs(template.children) do 530 | if newChildren[name] == nil then 531 | children[name] = elementFromTemplate(Roact, child, slowSelectors, fastSelectors) 532 | end 533 | end 534 | merge(children, newChildren, RoactTemplate.None) 535 | 536 | element = Roact.createElement(template.class, props, children) 537 | end 538 | 539 | if template.singleFragment then 540 | element = Roact.createFragment({ 541 | [template.instance.Name] = element, 542 | }) 543 | end 544 | 545 | return element 546 | end 547 | 548 | --[=[ 549 | @tag Extra 550 | @private 551 | Creates a Roact element from an internal template. 552 | ]=] 553 | function RoactTemplate.elementFromTemplate( 554 | Roact: Roact, 555 | template: TemplateItem, 556 | selectors: { [Selector]: Changes }? 557 | ): Element 558 | if selectors ~= nil and typeof(selectors) ~= "table" then 559 | warn("selectors object related to the following error message:", selectors) -- allows user inspection 560 | error("Expected a table { [Selector]: Changes } or nil for selectors, got " .. typeof(selectors)) 561 | end 562 | 563 | local slowSelectors: { [SelectorCallback]: any }? = nil 564 | local fastSelectors: { [string | Root]: any }? = nil 565 | if selectors ~= nil then 566 | slowSelectors = {} 567 | fastSelectors = {} 568 | assert(fastSelectors, "always") -- typechecker assert 569 | assert(slowSelectors, "always") -- typechecker assert 570 | 571 | for key, value in pairs(selectors) do 572 | if typeof(key) == "string" or key == RoactTemplate.Root then 573 | fastSelectors[key] = value 574 | elseif typeof(key) == "function" then 575 | slowSelectors[key] = value 576 | else 577 | error( 578 | ("Unknown Selector type " .. typeof(key) .. " (expected string | function | RoactTemplate.Root)\n") 579 | .. " Are you trying to specify props for the root instance of the template? Try the following instead:\n" 580 | .. " [RoactTemplate.Root] = { YourProps = 'Your Values' }" 581 | ) 582 | end 583 | 584 | if typeof(value) ~= "table" and typeof(value) ~= "function" then 585 | error( 586 | ("Unknown Changes type " .. typeof(value) .. " (expected table | function)\n") 587 | .. " Are you trying to specify props for the root instance of the template? Try the following instead:\n" 588 | .. " [RoactTemplate.Root] = { YourProps = 'Your Values' }" 589 | ) 590 | end 591 | end 592 | 593 | if next(slowSelectors) == nil then 594 | slowSelectors = nil 595 | end 596 | if next(fastSelectors) == nil then 597 | fastSelectors = nil 598 | end 599 | end 600 | 601 | return elementFromTemplate(Roact, template, slowSelectors, fastSelectors) 602 | end 603 | 604 | --[=[ 605 | @tag Extra 606 | @private 607 | Creates a Roact component from an internal template. 608 | ]=] 609 | function RoactTemplate.componentFromTemplate(Roact: Roact, template: TemplateItem): ({ [Selector]: Changes }) -> Element 610 | return function(selectors) 611 | return RoactTemplate.elementFromTemplate(Roact, template, selectors) 612 | end 613 | end 614 | 615 | return RoactTemplate 616 | -------------------------------------------------------------------------------- /src/packaged/init.lua: -------------------------------------------------------------------------------- 1 | return require(script.packages.lib) 2 | -------------------------------------------------------------------------------- /test/stories/assets.rbxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Corecii/roact-template/f8b71d6027d15cd17ee42bf6d187b6ee02377938/test/stories/assets.rbxm -------------------------------------------------------------------------------- /test/stories/changes.story/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This should: 3 | * Make the root frame transparent and blue-green 4 | * Set the title's text 5 | * Add effects to the title (gradient and ui stroke) 6 | * Set all ItemNameLabels' Texts 7 | * Modify items to have a background color based on their existing color 8 | ]] 9 | return function(target) 10 | local Roact = require(script.Parent.Parent.Roact) 11 | local RoactTemplate = require(script.Parent.Parent.lib) 12 | 13 | local ExampleTemplate = RoactTemplate.fromInstance(Roact, script.Parent.assets.simple) 14 | 15 | local function Example() 16 | return Roact.createElement(ExampleTemplate, { 17 | [RoactTemplate.Root] = { 18 | BackgroundTransparency = 0.5, 19 | BackgroundColor3 = Color3.fromRGB(0, 255, 255), 20 | }, 21 | TitleLabel = { 22 | Text = "Example Title Set With Changes", 23 | [Roact.Children] = { 24 | Gradient = Roact.createElement("UIGradient", { 25 | Color = ColorSequence.new(Color3.fromRGB(255, 255, 255), Color3.fromRGB(200, 200, 200)), 26 | }), 27 | UIStroke = Roact.createElement("UIStroke", { 28 | Thickness = 2, 29 | Color = Color3.fromRGB(200, 200, 200), 30 | }), 31 | }, 32 | }, 33 | ItemNameLabel = { Text = "Some Item" }, 34 | Item = function(instance) 35 | return { 36 | BackgroundColor3 = Color3.new( 37 | instance.BackgroundColor3.R * 0.8, 38 | instance.BackgroundColor3.G * 0.5, 39 | instance.BackgroundColor3.B * 0.8 40 | ), 41 | } 42 | end, 43 | }) 44 | end 45 | 46 | local tree = Roact.mount(Roact.createElement(Example), target) 47 | 48 | return function() 49 | Roact.unmount(tree) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/stories/samename.story/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This should delete all three "Item" children, since all three are placed 3 | under an "Item" fragment. 4 | ]] 5 | return function(target) 6 | local Roact = require(script.Parent.Parent.Roact) 7 | local RoactTemplate = require(script.Parent.Parent.lib) 8 | 9 | local ExampleTemplate = RoactTemplate.fromInstance(Roact, script.Parent.assets.simple) 10 | 11 | local function Example() 12 | return Roact.createElement(ExampleTemplate, { 13 | Content = { 14 | [Roact.Children] = { 15 | Item = RoactTemplate.None, 16 | }, 17 | }, 18 | }) 19 | end 20 | 21 | local tree = Roact.mount(Roact.createElement(Example), target) 22 | 23 | return function() 24 | Roact.unmount(tree) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/stories/selectors.story/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | All selectors should match and give their associated instances a child 3 | folder. 4 | ]] 5 | return function(target) 6 | local Roact = require(script.Parent.Parent.Roact) 7 | local RoactTemplate = require(script.Parent.Parent.lib) 8 | 9 | -- Current version of Rojo may not sync in attributes 10 | script.Parent.assets.selectors:FindFirstChild("Item", true):SetAttribute("Attribute", "test") 11 | 12 | local ExampleTemplate = RoactTemplate.fromInstance(Roact, script.Parent.assets.selectors) 13 | 14 | local function identifier(name) 15 | return { 16 | [Roact.Children] = { 17 | [name] = Roact.createElement("Folder"), 18 | }, 19 | } 20 | end 21 | 22 | local function Example() 23 | return Roact.createElement(ExampleTemplate, { 24 | TitleLabel = identifier("nameFast"), 25 | [RoactTemplate.Select.class("TextLabel")] = identifier("class"), 26 | [RoactTemplate.Select.isA("TextLabel")] = identifier("isA"), 27 | [RoactTemplate.Select.classPattern("^Text")] = identifier("classPattern"), 28 | [RoactTemplate.Select.name("Item")] = identifier("name"), 29 | [RoactTemplate.Select.namePattern("^Button")] = identifier("namePattern"), 30 | [RoactTemplate.Select.attribute("Attribute", "test")] = identifier("attribute"), 31 | [RoactTemplate.Select.prop("Text", "ExamplePropSelector")] = identifier("prop"), 32 | [RoactTemplate.Select.propPattern("Text", "^ExamplePropPatternSelector")] = identifier("propPattern"), 33 | [RoactTemplate.Select.tag("Example")] = identifier("tag"), 34 | [RoactTemplate.Select.every( 35 | RoactTemplate.Select.tag("EveryExample1"), 36 | RoactTemplate.Select.tag("EveryExample2") 37 | )] = identifier("every"), 38 | [RoactTemplate.Select.some( 39 | RoactTemplate.Select.tag("SomeExample1"), 40 | RoactTemplate.Select.tag("SomeExample2") 41 | )] = identifier("some"), 42 | [RoactTemplate.Select.no(RoactTemplate.Select.tag("NoExample"))] = identifier("no"), 43 | }) 44 | end 45 | 46 | local tree = Roact.mount(Roact.createElement(Example), target) 47 | 48 | return function() 49 | Roact.unmount(tree) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/stories/simple.story/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | An instance-for-instance recreation of the template instance should appear. 3 | ]] 4 | return function(target) 5 | local Roact = require(script.Parent.Parent.Roact) 6 | local RoactTemplate = require(script.Parent.Parent.lib) 7 | 8 | local ExampleTemplate = RoactTemplate.fromInstance(Roact, script.Parent.assets.simple) 9 | 10 | local tree = Roact.mount(Roact.createElement(ExampleTemplate), target) 11 | 12 | return function() 13 | Roact.unmount(tree) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/stories/wrapped.story/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Clicked count should go up when clicked, and render count should not. 3 | ]] 4 | return function(target) 5 | local Roact = require(script.Parent.Parent.Roact) 6 | local RoactTemplate = require(script.Parent.Parent.lib) 7 | 8 | local ExampleTemplate = RoactTemplate.fromInstance(Roact, script.Parent.assets.wrapped) 9 | 10 | local Counter = Roact.Component:extend("Counter") 11 | 12 | function Counter:init() 13 | self:setState({ 14 | count = 0, 15 | }) 16 | end 17 | 18 | function Counter:render() 19 | return Roact.createElement(self.props.template, { 20 | CountingButton = { 21 | Text = "Clicked: " .. self.state.count, 22 | [Roact.Event.Activated] = function() 23 | self:setState({ 24 | count = self.state.count + 1, 25 | }) 26 | end, 27 | }, 28 | }) 29 | end 30 | 31 | local renderCalls = 0 32 | 33 | local function Example() 34 | renderCalls += 1 35 | -- If components are working properly, "render 1" won't increase since 36 | -- the outer object isn't being re-rendered, only the button is! 37 | 38 | return Roact.createElement(ExampleTemplate, { 39 | TitleLabel = { Text = "A Counting Button with Internal State (render " .. renderCalls .. ")" }, 40 | CountingButton = RoactTemplate.wrapped(Counter), 41 | }) 42 | end 43 | 44 | local tree = Roact.mount(Roact.createElement(Example), target) 45 | 46 | return function() 47 | Roact.unmount(tree) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "corecii/api-dump-static" 7 | version = "1.0.5270372" 8 | dependencies = [] 9 | 10 | [[package]] 11 | name = "corecii/roact-template-tests" 12 | version = "0.1.0" 13 | dependencies = [["ApiDumpStatic", "corecii/api-dump-static@1.0.5270372"], ["Roact", "roblox/roact@1.4.2"]] 14 | 15 | [[package]] 16 | name = "roblox/roact" 17 | version = "1.4.2" 18 | dependencies = [] 19 | -------------------------------------------------------------------------------- /test/wally.toml: -------------------------------------------------------------------------------- 1 | # This file is necessary because the version of Wally we're using does not 2 | # handle combinding dev-dependencies and dependencies properly. 3 | 4 | [package] 5 | name = "corecii/roact-template-tests" 6 | version = "0.1.0" 7 | description = "Tests for roact-template" 8 | license = "MIT" 9 | registry = "https://github.com/UpliftGames/wally-index" 10 | realm = "shared" 11 | 12 | [dependencies] 13 | ApiDumpStatic = "corecii/api-dump-static@^1.0" 14 | Roact = "roblox/roact@1.4.2" -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "corecii/api-dump-static" 7 | version = "1.0.5270372" 8 | dependencies = [] 9 | 10 | [[package]] 11 | name = "corecii/roact-template" 12 | version = "0.2.0" 13 | dependencies = [["ApiDumpStatic", "corecii/api-dump-static@1.0.5270372"]] 14 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "corecii/roact-template" 3 | version = "0.2.0" 4 | description = "A runtime instance-to-Roact translator" 5 | license = "MIT" 6 | registry = "https://github.com/UpliftGames/wally-index" 7 | realm = "shared" 8 | exclude = ["*"] 9 | include = ["wally.toml", "src", "src/lib", "src/lib/**", "default.project.json", "README.md", 10 | "LICENSE", "moonwave.toml", "foreman.toml", "selene.toml", ".editorconfig"] 11 | 12 | [dependencies] 13 | ApiDumpStatic = "corecii/api-dump-static@^1.0" --------------------------------------------------------------------------------