├── .darklua.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .luau-analyze.json ├── .luaurc ├── .npmignore ├── .styluaignore ├── .vscode └── settings.json ├── .yarnrc.yml ├── LICENSE.txt ├── README.md ├── foreman.toml ├── jest.config.lua ├── package.json ├── packages ├── react-lua-hooks │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── SetStateType.lua │ │ ├── __tests__ │ │ ├── useDefaultState.test.lua │ │ ├── usePrevious.test.lua │ │ ├── usePreviousDistinct.test.lua │ │ ├── useToggle.test.lua │ │ └── useUnmount.test.lua │ │ ├── init.lua │ │ ├── useDebouncedState.lua │ │ ├── useDefaultState.lua │ │ ├── usePrevious.lua │ │ ├── usePreviousDistinct.lua │ │ ├── useTeardownEffect.lua │ │ ├── useThrottledState.lua │ │ ├── useToggle.lua │ │ └── useUnmount.lua ├── react-lua-use-constant │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── init.lua ├── react-render-hook │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── createRenderHook.lua │ │ └── init.lua ├── react-roblox-hooks │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── init.lua │ │ ├── useCamera.lua │ │ ├── useCameraCFrame.lua │ │ ├── useEvent.lua │ │ ├── useGuiSizeBinding.lua │ │ ├── useLocalPlayer.lua │ │ ├── useObjectLocation.lua │ │ ├── usePropertyChange.lua │ │ ├── useTaggedInstances.lua │ │ ├── useTextSize.lua │ │ └── useViewportSize.lua └── react-roblox-use-service │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ ├── ServiceProviderContext.lua │ ├── __tests__ │ └── useService.test.lua │ ├── init.lua │ └── useService.lua ├── roblox-test.server.lua ├── scripts ├── analyze.sh └── roblox-test.sh ├── selene.toml ├── selene_definitions.yml ├── stylua.toml ├── test-place.project.json └── yarn.lock /.darklua.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "rule": "convert_require", 5 | "current": { 6 | "name": "path", 7 | "sources": { 8 | "@pkg": "node_modules/.luau-aliases" 9 | } 10 | }, 11 | "target": { 12 | "name": "roblox", 13 | "rojo_sourcemap": "./sourcemap.json", 14 | "indexing_style": "wait_for_child" 15 | } 16 | }, 17 | { 18 | "rule": "inject_global_value", 19 | "identifier": "DEV", 20 | "value": true 21 | }, 22 | { 23 | "rule": "inject_global_value", 24 | "identifier": "__DEV__", 25 | "value": true 26 | }, 27 | { 28 | "rule": "inject_global_value", 29 | "identifier": "__ROACT_17_MOCK_SCHEDULER__", 30 | "value": true 31 | }, 32 | "compute_expression", 33 | "remove_unused_if_branch", 34 | "remove_unused_while", 35 | "filter_after_early_return", 36 | "remove_nil_declaration", 37 | "remove_empty_do" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.gif binary 4 | *.ico binary 5 | *.jpg binary 6 | *.png binary 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes #[issue number] 2 | 3 | 4 | 5 | - [ ] add entry to the changelog 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish-package: 7 | name: Publish package 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Enable corepack 13 | run: corepack enable 14 | 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: latest 18 | cache: yarn 19 | cache-dependency-path: yarn.lock 20 | 21 | - name: Install packages 22 | run: yarn install --immutable 23 | 24 | - name: Run npmluau 25 | run: yarn run prepare 26 | 27 | - name: Authenticate yarn 28 | run: |- 29 | yarn config set npmAlwaysAuth true 30 | yarn config set npmAuthToken $NPM_AUTH_TOKEN 31 | env: 32 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | 34 | - name: Publish to npm 35 | run: yarn workspaces foreach --all --no-private npm publish --access public --tolerate-republish 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Run tests 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: Roblox/setup-foreman@v1 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: Enable Corepack 24 | run: corepack enable 25 | 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: "latest" 29 | cache: "yarn" 30 | cache-dependency-path: "yarn.lock" 31 | 32 | - name: Install packages 33 | run: yarn install --immutable 34 | 35 | - name: Run npmluau 36 | run: yarn run prepare 37 | 38 | - name: Run linter 39 | run: yarn run lint 40 | 41 | - name: Verify code style 42 | run: yarn run style-check 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /site 2 | /*.rbxl 3 | /*.rbxlx 4 | /*.rbxl.lock 5 | /*.rbxlx.lock 6 | /*.rbxm 7 | /*.rbxmx 8 | 9 | /jest.lua 10 | /roblox 11 | /node_modules 12 | 13 | /.yarn 14 | 15 | build/ 16 | test-places/ 17 | 18 | /globalTypes.d.lua 19 | 20 | **/sourcemap.json 21 | -------------------------------------------------------------------------------- /.luau-analyze.json: -------------------------------------------------------------------------------- 1 | { 2 | "luau-lsp.require.mode": "relativeToFile", 3 | "luau-lsp.require.directoryAliases": { 4 | "@pkg": "node_modules/.luau-aliases" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.luaurc: -------------------------------------------------------------------------------- 1 | { 2 | "languageMode": "strict", 3 | "lintErrors": true, 4 | "lint": { 5 | "*": true, 6 | "LocalShadow": false 7 | } 8 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /scripts/ 4 | /docs/ 5 | 6 | /roblox 7 | /build 8 | 9 | .gitattributes 10 | CHANGELOG.md 11 | 12 | .yarn 13 | 14 | .darklua* 15 | .luau-analyze.json 16 | .luaurc 17 | foreman.toml 18 | selene.toml 19 | selene_definitions.yml 20 | stylua.toml 21 | .styluaignore 22 | 23 | /globalTypes.d.lua 24 | **/sourcemap.json 25 | *.project.json 26 | 27 | **/__tests__ 28 | **/*.test.lua 29 | **/jest.config.lua 30 | 31 | **/*.rbxl 32 | **/*.rbxlx 33 | **/*.rbxl.lock 34 | **/*.rbxlx.lock 35 | **/*.rbxm 36 | **/*.rbxmx 37 | -------------------------------------------------------------------------------- /.styluaignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /roblox 3 | 4 | **/*.d.lua 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "luau-lsp.require.directoryAliases": { 3 | "@pkg": "node_modules/.luau-aliases" 4 | } 5 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sea of Voices 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![checks](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml/badge.svg)](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml) 4 | [![GitHub top language](https://img.shields.io/github/languages/top/seaofvoices/react-lua-hooks)](https://github.com/luau-lang/luau) 5 | ![version](https://img.shields.io/npm/v/@seaofvoices/react-lua-hooks?label=react-lua-hooks) 6 | ![version](https://img.shields.io/npm/v/@seaofvoices/react-lua-hooks?label=react-roblox-hooks) 7 | ![license](https://img.shields.io/npm/l/@seaofvoices/react-lua-hooks) 8 | ![npm](https://img.shields.io/npm/dt/@seaofvoices/react-lua-hooks?label=react-lua-hooks%20downloads) 9 | ![npm](https://img.shields.io/npm/dt/@seaofvoices/react-roblox-hooks?label=react-roblox-hooks%20downloads) 10 | 11 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/seaofvoices) 12 | 13 |
14 | 15 | # React Lua Hooks 16 | 17 | This project consists of three main packages that revolves around [react-lua](https://github.com/jsdotlua/react-lua) hooks: 18 | 19 | - [Lua hooks](packages/react-lua-hooks/README.md#content): general-purpose collection of hooks for [react-lua](https://github.com/jsdotlua/react-lua) 20 | - [Roblox hooks](packages/react-roblox-hooks/README.md#content): hooks specifically made for Roblox development 21 | - [react-render-hook](packages/react-render-hook/README.md#content): a utility function to quickly test React hooks 22 | 23 | # Installation 24 | 25 | Add these packages to your dependencies: 26 | 27 | ```bash 28 | yarn add @seaofvoices/react-lua-hooks 29 | yarn add @seaofvoices/react-roblox-hooks 30 | 31 | yarn add -D @seaofvoices/react-render-hook 32 | ``` 33 | 34 | Or if you are using `npm`: 35 | 36 | ```bash 37 | npm install @seaofvoices/react-lua-hooks 38 | npm install @seaofvoices/react-roblox-hooks 39 | 40 | npm install @seaofvoices/react-render-hook --save-dev 41 | ``` 42 | 43 | # Content 44 | 45 | - [Lua hooks](packages/react-lua-hooks/README.md#content) 46 | - [useConstant](packages/react-lua-use-constant/README.md#usage) 47 | - [useDefaultState](packages/react-lua-hooks/README.md#usedefaultstate) 48 | - [usePrevious](packages/react-lua-hooks/README.md#useprevious) 49 | - [usePreviousDistinct](packages/react-lua-hooks/README.md#usepreviousdistinct) 50 | - [useTeardownEffect](packages/react-lua-hooks/README.md#useteardowneffect) 51 | - [useToggle](packages/react-lua-hooks/README.md#usetoggle) 52 | - [useUnmount](packages/react-lua-hooks/README.md#useunmount) 53 | - [useDebouncedState](packages/react-lua-hooks/README.md#usedebouncedstate) 54 | - [useThrottledState](packages/react-lua-hooks/README.md#usethrottledstate) 55 | - [Roblox hooks](packages/react-roblox-hooks/README.md#content) 56 | - [useService](packages/react-roblox-hooks/README.md#useservice) 57 | - [useCamera](packages/react-roblox-hooks/README.md#usecamera) 58 | - [useCameraCFrame](packages/react-roblox-hooks/README.md#usecameracframe) 59 | - [useEvent](packages/react-roblox-hooks/README.md#useevent) 60 | - [useGuiObjectSizeBinding](packages/react-roblox-hooks/README.md#useguiobjectsizebinding) 61 | - [useLocalPlayer](packages/react-roblox-hooks/README.md#uselocalplayer) 62 | - [useObjectLocation](packages/react-roblox-hooks/README.md#useobjectlocation) 63 | - [usePropertyChange](packages/react-roblox-hooks/README.md#usepropertychange) 64 | - [useTaggedInstances](packages/react-roblox-hooks/README.md#usetaggedinstances) 65 | - [useTextSize](packages/react-roblox-hooks/README.md#usetextsize) 66 | - [useViewportSize](packages/react-roblox-hooks/README.md#useviewportsize) 67 | - [react-render-hook](packages/react-render-hook/README.md#content) 68 | - [renderHook](packages/react-render-hook/README.md#renderhook) 69 | - [createRenderHook](packages/react-render-hook/README.md#createrenderhook) 70 | 71 | # Other Lua Environments Support 72 | 73 | If you would like to use this library on a Lua environment where it is currently incompatible, open an issue (or comment on an existing one) to request the appropriate modifications. 74 | 75 | The library uses [darklua](https://github.com/seaofvoices/darklua) to process its code. 76 | -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rojo = { github = "rojo-rbx/rojo", version = "=7.4.4"} 3 | selene = { github = "Kampfkarren/selene", version = "=0.27.1"} 4 | stylua = { github = "JohnnyMorganz/StyLua", version = "=0.20.0"} 5 | darklua = { github = "seaofvoices/darklua", version = "=0.14.0" } 6 | luau-lsp = { github = "JohnnyMorganz/luau-lsp", version = "=1.35.0"} 7 | run-in-roblox = { github = "rojo-rbx/run-in-roblox", version = "=0.3.0" } 8 | -------------------------------------------------------------------------------- /jest.config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | testMatch = { '**/*.test' }, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "prepare": "npmluau", 9 | "lint": "sh ./scripts/analyze.sh && selene packages", 10 | "lint:luau": "sh ./scripts/analyze.sh", 11 | "lint:selene": "selene packages", 12 | "format": "stylua .", 13 | "style-check": "stylua . --check", 14 | "test:roblox": "sh ./scripts/roblox-test.sh", 15 | "verify-pack": "yarn workspaces foreach -A --no-private pack --dry-run", 16 | "clean": "rm -rf node_modules" 17 | }, 18 | "devDependencies": { 19 | "@jsdotlua/jest": "^3.6.1-rc.2", 20 | "@jsdotlua/jest-globals": "^3.6.1-rc.2", 21 | "npmluau": "^0.1.1" 22 | }, 23 | "packageManager": "yarn@4.0.2" 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /scripts/ 4 | /docs/ 5 | 6 | /roblox 7 | /build 8 | 9 | .gitattributes 10 | CHANGELOG.md 11 | 12 | .yarn 13 | 14 | .darklua* 15 | .luau-analyze.json 16 | .luaurc 17 | foreman.toml 18 | selene.toml 19 | selene_definitions.yml 20 | stylua.toml 21 | .styluaignore 22 | 23 | /globalTypes.d.lua 24 | **/sourcemap.json 25 | *.project.json 26 | 27 | **/__tests__ 28 | **/*.test.lua 29 | **/jest.config.lua 30 | 31 | **/*.rbxl 32 | **/*.rbxlx 33 | **/*.rbxl.lock 34 | **/*.rbxlx.lock 35 | **/*.rbxm 36 | **/*.rbxmx 37 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.1 4 | 5 | * fix `useUnmount` hook ([#6](https://github.com/seaofvoices/react-lua-hooks/pull/6)) 6 | * fix `usePrevious` and `usePreviousDistinct` hooks ([#5](https://github.com/seaofvoices/react-lua-hooks/pull/5)) 7 | 8 | ## 0.1.0 9 | 10 | * Initial version 11 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![checks](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml/badge.svg)](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml) 4 | ![version](https://img.shields.io/npm/v/@seaofvoices/react-lua-hooks?label=version) 5 | [![GitHub top language](https://img.shields.io/github/languages/top/seaofvoices/react-lua-hooks)](https://github.com/luau-lang/luau) 6 | ![license](https://img.shields.io/npm/l/@seaofvoices/react-lua-hooks) 7 | ![npm](https://img.shields.io/npm/dt/@seaofvoices/react-lua-hooks) 8 | 9 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/seaofvoices) 10 | 11 |
12 | 13 | # react-lua-hooks 14 | 15 | This package is a collection of hooks for [react-lua](https://github.com/jsdotlua/react-lua). 16 | 17 | ## Installation 18 | 19 | Add `react-lua-hooks` in your dependencies: 20 | 21 | ```bash 22 | yarn add @seaofvoices/react-lua-hooks 23 | ``` 24 | 25 | Or if you are using `npm`: 26 | 27 | ```bash 28 | npm install @seaofvoices/react-lua-hooks 29 | ``` 30 | 31 | ## Content 32 | 33 | **Hooks:** 34 | 35 | - [useDefaultState](#usedefaultstate) 36 | - [useTeardownEffect](#useteardowneffect) 37 | - [usePrevious](#useprevious) 38 | - [usePreviousDistinct](#usepreviousdistinct) 39 | - [useToggle](#usetoggle) 40 | - [useUnmount](#useunmount) 41 | - [useDebouncedState](#usedebouncedstate) 42 | - [useThrottledState](#usethrottledstate) 43 | 44 | ### `useDefaultState` 45 | 46 | A hook that wraps `useState` to provides a default value whenever the actual state value is `nil`. 47 | 48 | ```lua 49 | function useDefaultState(initialValue: T?, defaultValue: T): (T, (T) -> ()) 50 | ``` 51 | 52 | #### Example 53 | 54 | ```lua 55 | local function Component(props) 56 | local value, setValue = useDefaultState(nil, 10) 57 | end 58 | ``` 59 | 60 | ### `useTeardownEffect` 61 | 62 | A hook that wraps `useEffect` to handle multiple types of values that can be cleaned up with [luau-teardown](https://github.com/seaofvoices/luau-teardown). 63 | 64 | The `useEffect` callback must absolutely return a single function for the clean up. The `useTeardownEffect` is more flexible, as you can return any number of values supported by `luau-teardown` (`thread`, functions, `Instance`, `RBXScriptConnection`, etc.) 65 | 66 | ```lua 67 | function useTeardownEffect(effect: () -> ...Teardown, deps: { any }?) 68 | ``` 69 | 70 | #### Example 71 | 72 | ```lua 73 | local function Component(props) 74 | local target = props.target 75 | 76 | useTeardownEffect(function() 77 | local container = Instance.new("Folder") 78 | container.Parent = workspace 79 | 80 | return 81 | container, 82 | target.ChildAdded:Connect(function() 83 | -- ... 84 | end), 85 | task.delay(1, function() 86 | -- ... 87 | end) 88 | end, { target }) 89 | 90 | -- the example above is equivalent to this useEffect 91 | useEffect(function() 92 | local container = Instance.new("Folder") 93 | container.Parent = workspace 94 | 95 | local connection = target.ChildAdded:Connect(function() 96 | -- ... 97 | end) 98 | 99 | local thread = task.delay(1, function() 100 | -- ... 101 | end) 102 | 103 | return function() 104 | container:Destroy() 105 | connection:Disconnect() 106 | task.cancel( thread) 107 | end 108 | end, { target }) 109 | end 110 | ``` 111 | 112 | ### `usePrevious` 113 | 114 | A hook that returns the previous value of a variable. Use this hook to track changes over renders and perform actions based on the previous state. 115 | 116 | ```lua 117 | function usePrevious(value: T): T? 118 | ``` 119 | 120 | #### Example 121 | 122 | ```lua 123 | local function Component(props) 124 | local currentValue = ... 125 | local previousValue = usePrevious(currentValue) 126 | end 127 | ``` 128 | 129 | ### `usePreviousDistinct` 130 | 131 | Similar to [`usePrevious`](#useprevious), this hook returns the previous distinct (non-equal) value of a state or variable. It is useful when you want to ignore consecutive identical values. 132 | 133 | Value comparison is done using `==`, but a function can be passed to customize the equality check. 134 | 135 | ```lua 136 | function usePreviousDistinct(value: T): T? 137 | function usePreviousDistinct(value: T, isEqual: ((T, T) -> boolean)): T? 138 | ``` 139 | 140 | #### Example 141 | 142 | ```lua 143 | local function Component(props) 144 | local value = useSomeState() 145 | local previousDistinctValue = usePreviousDistinct(value) 146 | end 147 | ``` 148 | 149 | ### `useToggle` 150 | 151 | A hook to manage a boolean value. 152 | 153 | ```lua 154 | function useToggle(defaultValue: boolean?): (boolean, Toggle) 155 | -- where 156 | type Toggle = { 157 | toggle: () -> (), 158 | on: () -> (), 159 | off: () -> (), 160 | } 161 | ``` 162 | 163 | #### Example 164 | 165 | ```lua 166 | local function Component(props) 167 | local value, toggle = useToggle(false) 168 | 169 | -- Somewhere in your component 170 | toggle.toggle() -- toggles the value 171 | toggle.on() -- set the value to true 172 | toggle.off() -- set the value to false 173 | end 174 | ``` 175 | 176 | ### `useUnmount` 177 | 178 | A hook that executes a callback when a component is unmounted. 179 | 180 | ```lua 181 | function useUnmount(fn: () -> ()) 182 | ``` 183 | 184 | #### Example 185 | 186 | ```lua 187 | local function Component(props) 188 | useUnmount(function() 189 | -- cleanup logic when the component is unmounted 190 | end) 191 | end 192 | ``` 193 | 194 | ### `useDebouncedState` 195 | 196 | A hook that returns a debounced version of the state, ensuring that a state update is delayed until a specified time has passed without further updates. 197 | 198 | ```lua 199 | function useDebouncedState(initialValue: T, intervalSeconds: number): (T, (T) -> ()) 200 | ``` 201 | 202 | #### Example 203 | 204 | ```lua 205 | local function Component(props) 206 | -- debounce for 0.5 seconds 207 | local value, setValue = useDebouncedState(1, 0.5) 208 | end 209 | ``` 210 | 211 | ### `useThrottledState` 212 | 213 | Similar to `useDebouncedState`, this hook throttles state updates, but instead of delaying updates, it limits the rate at which updates occur. 214 | 215 | ```lua 216 | function useThrottledState(initialValue: T, intervalSeconds: number): (T, (T) -> ()) 217 | ``` 218 | 219 | #### Example 220 | 221 | ```lua 222 | local function Component(props) 223 | -- throttle updates to once every second 224 | local value, setValue = useThrottledState(someValue, 1) 225 | -- `setValue` 226 | end 227 | ``` 228 | 229 | ## Other Lua Environments Support 230 | 231 | If you would like to use this library on a Lua environment, where it is currently incompatible, open an issue (or comment on an existing one) to request the appropriate modifications. 232 | 233 | The library uses [darklua](https://github.com/seaofvoices/darklua) to process its code. 234 | 235 | ## License 236 | 237 | This project is available under the MIT license. See [LICENSE.txt](../../LICENSE.txt) for details. 238 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seaofvoices/react-lua-hooks", 3 | "description": "A collection of React hook for Luau", 4 | "version": "0.1.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/seaofvoices/react-lua-hooks.git", 8 | "directory": "packages/react-lua-hooks" 9 | }, 10 | "keywords": [ 11 | "lua", 12 | "luau", 13 | "react", 14 | "roblox", 15 | "hook" 16 | ], 17 | "license": "MIT", 18 | "main": "./src/init.lua", 19 | "dependencies": { 20 | "@jsdotlua/react": "^17.1.0", 21 | "@seaofvoices/react-lua-use-constant": "workspace:^", 22 | "luau-teardown": "^0.1.4" 23 | }, 24 | "devDependencies": { 25 | "@jsdotlua/jest-globals": "^3.6.1-rc.2", 26 | "@jsdotlua/react-testing-library": "^12.2.1-rc.1", 27 | "@seaofvoices/react-render-hook": "workspace:^", 28 | "npmluau": "^0.1.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/SetStateType.lua: -------------------------------------------------------------------------------- 1 | type SetStateValue = (T) -> () 2 | type UpdateStateValue = ((T) -> T) -> () 3 | 4 | export type SetState = SetStateValue & UpdateStateValue 5 | 6 | return nil 7 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/__tests__/useDefaultState.test.lua: -------------------------------------------------------------------------------- 1 | local jestGlobals = require('@pkg/@jsdotlua/jest-globals') 2 | local renderHook = require('@pkg/@seaofvoices/react-render-hook').renderHook 3 | local ReactTesting = require('@pkg/@jsdotlua/react-testing-library') 4 | 5 | local useDefaultState = require('../useDefaultState') 6 | 7 | local expect = jestGlobals.expect 8 | local it = jestGlobals.it 9 | local act = ReactTesting.act 10 | 11 | it('returns the default value when the value is nil', function() 12 | local renderResult = renderHook(function() 13 | return useDefaultState(nil, 'default') 14 | end) 15 | 16 | expect(renderResult.result.current[1]).toBe('default') 17 | end) 18 | 19 | it('returns the value when it is not nil', function() 20 | local renderResult = renderHook(function() 21 | return useDefaultState('initial', 'default') 22 | end) 23 | 24 | expect(renderResult.result.current[1]).toBe('initial') 25 | end) 26 | 27 | it('updates the value correctly with setState', function() 28 | local renderResult = renderHook(function() 29 | return useDefaultState(nil, 'default') 30 | end) 31 | 32 | local setValue = renderResult.result.current[2] 33 | 34 | act(function() 35 | setValue('updated') 36 | end) 37 | 38 | expect(renderResult.result.current[1]).toBe('updated') 39 | end) 40 | 41 | it('returns the default value when setState is called with nil', function() 42 | local renderResult = renderHook(function() 43 | return useDefaultState(nil, 'default') 44 | end) 45 | 46 | local setValue = renderResult.result.current[2] 47 | 48 | act(function() 49 | setValue(nil) 50 | end) 51 | 52 | expect(renderResult.result.current[1]).toBe('default') 53 | end) 54 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/__tests__/usePrevious.test.lua: -------------------------------------------------------------------------------- 1 | local jestGlobals = require('@pkg/@jsdotlua/jest-globals') 2 | local renderHook = require('@pkg/@seaofvoices/react-render-hook').renderHook 3 | 4 | local usePrevious = require('../usePrevious') 5 | 6 | local expect = jestGlobals.expect 7 | local it = jestGlobals.it 8 | 9 | it('it initially returns nil', function() 10 | local renderResult = renderHook(usePrevious :: any, 'text') 11 | 12 | expect(renderResult.result.current).toBe(nil) 13 | end) 14 | 15 | it('returns the initial value after the first re-render', function() 16 | local renderResult = renderHook(usePrevious :: any, 'text') 17 | 18 | expect(renderResult.result.current).toBe(nil) 19 | 20 | renderResult.rerender('text 2') 21 | 22 | expect(renderResult.result.current).toBe('text') 23 | end) 24 | 25 | it('returns the previous value after multiple re-render', function() 26 | local renderResult = renderHook(usePrevious :: any, 'text') 27 | expect(renderResult.result.current).toBe(nil) 28 | 29 | renderResult.rerender('text 2') 30 | expect(renderResult.result.current).toBe('text') 31 | 32 | renderResult.rerender('text 3') 33 | expect(renderResult.result.current).toBe('text 2') 34 | 35 | renderResult.rerender('text 3') 36 | expect(renderResult.result.current).toBe('text 3') 37 | end) 38 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/__tests__/usePreviousDistinct.test.lua: -------------------------------------------------------------------------------- 1 | local jestGlobals = require('@pkg/@jsdotlua/jest-globals') 2 | local renderHook = require('@pkg/@seaofvoices/react-render-hook').renderHook 3 | 4 | local usePreviousDistinct = require('../usePreviousDistinct') 5 | 6 | local expect = jestGlobals.expect 7 | local it = jestGlobals.it 8 | 9 | local function compareStringBeginning(a: string, b: string): boolean 10 | return string.sub(a, 1, 3) == string.sub(b, 1, 3) 11 | end 12 | 13 | it('initially returns nil', function() 14 | local renderResult = renderHook(usePreviousDistinct, 'text') 15 | 16 | expect(renderResult.result.current).toBe(nil) 17 | end) 18 | 19 | it('returns the previous value after a re-render when no compare function is provided', function() 20 | local renderResult = renderHook(usePreviousDistinct, 'text') 21 | 22 | expect(renderResult.result.current).toBe(nil) 23 | 24 | renderResult.rerender('text 2') 25 | expect(renderResult.result.current).toBe('text') 26 | 27 | renderResult.rerender('text 3') 28 | expect(renderResult.result.current).toBe('text 2') 29 | end) 30 | 31 | it.only( 32 | 'returns the same previous value after a re-render if the new value is equal to the last (with a compare function)', 33 | function() 34 | local renderResult = renderHook(usePreviousDistinct, 'abc', compareStringBeginning) 35 | 36 | renderResult.rerender('oof', compareStringBeginning) 37 | expect(renderResult.result.current).toBe('abc') 38 | 39 | renderResult.rerender('oof^2', compareStringBeginning) 40 | expect(renderResult.result.current).toBe('abc') 41 | 42 | renderResult.rerender('hello', compareStringBeginning) 43 | expect(renderResult.result.current).toBe('oof') 44 | 45 | renderResult.rerender('', compareStringBeginning) 46 | expect(renderResult.result.current).toBe('hello') 47 | end 48 | ) 49 | 50 | it( 51 | 'returns the same previous value after a re-render if the new value is equal to the last', 52 | function() 53 | local renderResult = renderHook(usePreviousDistinct, 0) 54 | 55 | renderResult.rerender(1) 56 | expect(renderResult.result.current).toBe(0) 57 | 58 | renderResult.rerender(1) 59 | expect(renderResult.result.current).toBe(0) 60 | 61 | renderResult.rerender(2) 62 | expect(renderResult.result.current).toBe(1) 63 | end 64 | ) 65 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/__tests__/useToggle.test.lua: -------------------------------------------------------------------------------- 1 | local jestGlobals = require('@pkg/@jsdotlua/jest-globals') 2 | local renderHook = require('@pkg/@seaofvoices/react-render-hook').renderHook 3 | local ReactTesting = require('@pkg/@jsdotlua/react-testing-library') 4 | 5 | local useToggle = require('../useToggle') 6 | 7 | local expect = jestGlobals.expect 8 | local it = jestGlobals.it 9 | local describe = jestGlobals.describe 10 | local beforeEach = jestGlobals.beforeEach 11 | local act = ReactTesting.act 12 | 13 | it('initializes to true', function() 14 | local renderResult = renderHook(useToggle :: any, true) 15 | 16 | expect(renderResult.result.current[1]).toBe(true) 17 | end) 18 | 19 | it('initializes to false', function() 20 | local renderResult = renderHook(useToggle :: any, false) 21 | 22 | expect(renderResult.result.current[1]).toBe(false) 23 | end) 24 | 25 | describe('default toggle', function() 26 | local state 27 | local toggle 28 | local renderResult 29 | 30 | beforeEach(function() 31 | renderResult = renderHook(useToggle :: any) 32 | local current = renderResult.result.current 33 | state = current[1] 34 | toggle = current[2] 35 | end) 36 | 37 | it('initializes to false if no default value', function() 38 | expect(state).toBe(false) 39 | end) 40 | 41 | it('toggles the value when toggle is called', function() 42 | act(function() 43 | toggle.toggle() 44 | end) 45 | expect(renderResult.result.current[1]).toBe(true) 46 | 47 | act(function() 48 | toggle.toggle() 49 | end) 50 | expect(renderResult.result.current[1]).toBe(false) 51 | end) 52 | 53 | it('sets the value to true when on is called', function() 54 | act(function() 55 | toggle.on() 56 | end) 57 | expect(renderResult.result.current[1]).toBe(true) 58 | 59 | act(function() 60 | toggle.on() 61 | end) 62 | expect(renderResult.result.current[1]).toBe(true) 63 | end) 64 | 65 | it('sets the value to false when off is called', function() 66 | act(function() 67 | toggle.off() 68 | end) 69 | expect(renderResult.result.current[1]).toBe(false) 70 | 71 | act(function() 72 | toggle.off() 73 | end) 74 | expect(renderResult.result.current[1]).toBe(false) 75 | end) 76 | end) 77 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/__tests__/useUnmount.test.lua: -------------------------------------------------------------------------------- 1 | local jestGlobals = require('@pkg/@jsdotlua/jest-globals') 2 | local renderHook = require('@pkg/@seaofvoices/react-render-hook').renderHook 3 | 4 | local useUnmount = require('../useUnmount') 5 | 6 | local expect = jestGlobals.expect 7 | local it = jestGlobals.it 8 | local jest = jestGlobals.jest 9 | 10 | it('calls the provided cleanup function on unmount', function() 11 | local cleanupMock = jest.fn() 12 | 13 | local renderResult = renderHook(useUnmount, cleanupMock) 14 | 15 | renderResult.unmount() 16 | 17 | expect(cleanupMock).toHaveBeenCalledTimes(1) 18 | end) 19 | 20 | it('does not call the cleanup function on rerender', function() 21 | local cleanupMock = jest.fn() 22 | 23 | local renderResult = renderHook(useUnmount, cleanupMock) 24 | 25 | renderResult.rerender(cleanupMock) 26 | 27 | expect(cleanupMock).toHaveBeenCalledTimes(0) 28 | end) 29 | 30 | it('calls the last cleanup function on unmount', function() 31 | local cleanupMock = jest.fn() 32 | local lastCleanupMock = jest.fn() 33 | 34 | local renderResult = renderHook(useUnmount, cleanupMock) 35 | 36 | renderResult.rerender(lastCleanupMock) 37 | 38 | renderResult.unmount() 39 | 40 | expect(cleanupMock).toHaveBeenCalledTimes(0) 41 | expect(lastCleanupMock).toHaveBeenCalledTimes(1) 42 | end) 43 | 44 | it('does not throw if no function is provided', function() 45 | local renderResult = renderHook(useUnmount) 46 | 47 | expect(function() 48 | renderResult.unmount() 49 | end).never.toThrow() 50 | end) 51 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/init.lua: -------------------------------------------------------------------------------- 1 | local useConstant = require('@pkg/@seaofvoices/react-lua-use-constant') 2 | 3 | local useDebouncedState = require('./useDebouncedState') 4 | local useDefaultState = require('./useDefaultState') 5 | local usePrevious = require('./usePrevious') 6 | local usePreviousDistinct = require('./usePreviousDistinct') 7 | local useTeardownEffect = require('./useTeardownEffect') 8 | local useThrottledState = require('./useThrottledState') 9 | local useToggle = require('./useToggle') 10 | local useUnmount = require('./useUnmount') 11 | 12 | export type Toggle = useToggle.Toggle 13 | 14 | return { 15 | useConstant = useConstant, 16 | useDebouncedState = useDebouncedState, 17 | useDefaultState = useDefaultState, 18 | usePrevious = usePrevious, 19 | usePreviousDistinct = usePreviousDistinct, 20 | useTeardownEffect = useTeardownEffect, 21 | useThrottledState = useThrottledState, 22 | useToggle = useToggle, 23 | useUnmount = useUnmount, 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/useDebouncedState.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local SetStateType = require('./SetStateType') 4 | local useUnmount = require('./useUnmount') 5 | 6 | local useCallback = React.useCallback 7 | local useEffect = React.useEffect 8 | local useRef = React.useRef 9 | local useState = React.useState 10 | 11 | type SetState = SetStateType.SetState 12 | 13 | local function useDebouncedState(initialValue: T, intervalSeconds: number): (T, SetState) 14 | local value, setValue = useState(initialValue) 15 | 16 | local scheduledValue = useRef(nil :: { value: T | (T) -> T }?) 17 | local lastCallTime = useRef(nil :: number?) 18 | local scheduledUpdate = useRef(nil :: thread?) 19 | 20 | local setDebouncedValue = useCallback(function(newValue: T | (T) -> T) 21 | local update = scheduledUpdate.current 22 | if update then 23 | task.cancel(update) 24 | end 25 | 26 | lastCallTime.current = os.clock() 27 | scheduledValue.current = { value = newValue } 28 | scheduledUpdate.current = task.delay(intervalSeconds, function() 29 | lastCallTime.current = nil 30 | scheduledUpdate.current = nil 31 | setValue(newValue) 32 | end) 33 | end, { setValue :: any, intervalSeconds }) 34 | 35 | useEffect(function() 36 | local lastCall = lastCallTime.current 37 | local update = scheduledUpdate.current 38 | local nextValue = scheduledValue.current 39 | 40 | if update and lastCall and nextValue then 41 | scheduledUpdate.current = nil 42 | task.cancel(update) 43 | 44 | if os.clock() >= lastCall + intervalSeconds then 45 | setValue(nextValue.value) 46 | else 47 | scheduledUpdate.current = task.delay(intervalSeconds, function() 48 | lastCallTime.current = nil 49 | scheduledUpdate.current = nil 50 | 51 | setValue(nextValue.value) 52 | end) 53 | end 54 | end 55 | end, { intervalSeconds }) 56 | 57 | useUnmount(function() 58 | local update = scheduledUpdate.current 59 | if update then 60 | task.cancel(update) 61 | end 62 | scheduledValue.current = nil 63 | lastCallTime.current = nil 64 | end) 65 | 66 | return value, setDebouncedValue 67 | end 68 | 69 | return useDebouncedState 70 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/useDefaultState.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local SetStateType = require('./SetStateType') 4 | 5 | local useState = React.useState 6 | 7 | type SetState = SetStateType.SetState 8 | 9 | local function useDefaultState(initialValue: T?, defaultValue: T): (T, SetState) 10 | local value, setValue = useState(initialValue) 11 | 12 | return if value == nil then defaultValue else value, setValue 13 | end 14 | 15 | return useDefaultState 16 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/usePrevious.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useRef = React.useRef 4 | local useEffect = React.useEffect 5 | 6 | local function usePrevious(value: T): T? 7 | local ref = useRef(nil :: T?) 8 | 9 | useEffect(function() 10 | ref.current = value 11 | end) 12 | 13 | return ref.current 14 | end 15 | 16 | return usePrevious 17 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/usePreviousDistinct.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useRef = React.useRef 4 | 5 | local function usePreviousDistinct(value: T, isEqual: ((T, T) -> boolean)?): T? 6 | local previousRef = useRef(nil :: { value: T }?) 7 | local currentRef = useRef(nil :: { value: T }?) 8 | 9 | if currentRef.current == nil then 10 | currentRef.current = { value = value } 11 | else 12 | local currentIsEqual = if isEqual ~= nil and previousRef.current ~= nil 13 | then isEqual(value, currentRef.current.value) 14 | elseif previousRef.current == nil then false 15 | else value == currentRef.current.value 16 | 17 | if not currentIsEqual then 18 | previousRef.current = { value = currentRef.current.value } 19 | currentRef.current = { value = value } 20 | end 21 | end 22 | 23 | return previousRef.current and previousRef.current.value 24 | end 25 | 26 | return usePreviousDistinct 27 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/useTeardownEffect.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | local Teardown = require('@pkg/luau-teardown') 3 | 4 | type Teardown = Teardown.Teardown 5 | 6 | local useEffect = React.useEffect 7 | 8 | local function useTeardownEffect(effect: () -> ...Teardown, deps: { any }?): () 9 | useEffect(function() 10 | return Teardown.fn(effect()) 11 | end, deps) 12 | end 13 | 14 | return useTeardownEffect 15 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/useThrottledState.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local SetStateType = require('./SetStateType') 4 | local useUnmount = require('./useUnmount') 5 | 6 | local useCallback = React.useCallback 7 | local useEffect = React.useEffect 8 | local useRef = React.useRef 9 | local useState = React.useState 10 | 11 | type SetState = SetStateType.SetState 12 | 13 | local function useThrottledState(initialValue: T, intervalSeconds: number): (T, SetState) 14 | local value, setValue = useState(initialValue) 15 | local lastThrottleTime = useRef(os.clock()) 16 | 17 | local scheduledValue = useRef(nil :: { value: T | (T) -> T }?) 18 | local scheduledUpdate = useRef(nil :: thread?) 19 | 20 | local setThrottledValue = useCallback(function(newValue: T | (T) -> T) 21 | local remainingTime = intervalSeconds - (os.clock() - lastThrottleTime.current :: number) 22 | 23 | if remainingTime <= 0 then 24 | lastThrottleTime.current = os.clock() 25 | setValue(newValue) 26 | return 27 | else 28 | scheduledValue.current = { value = newValue } 29 | if scheduledUpdate.current == nil then 30 | scheduledUpdate.current = task.delay(remainingTime, function() 31 | scheduledUpdate.current = nil 32 | local current = scheduledValue.current 33 | 34 | if current then 35 | scheduledValue.current = nil 36 | 37 | lastThrottleTime.current = os.clock() 38 | setValue(current.value) 39 | end 40 | end) 41 | end 42 | end 43 | end, { setValue :: any, intervalSeconds }) 44 | 45 | useEffect(function() 46 | local update = scheduledUpdate.current 47 | local nextValue = scheduledValue.current 48 | if update and nextValue then 49 | scheduledUpdate.current = nil 50 | task.cancel(update) 51 | setThrottledValue(nextValue.value) 52 | end 53 | end, { setThrottledValue }) 54 | 55 | useUnmount(function() 56 | local update = scheduledUpdate.current 57 | if update then 58 | task.cancel(update) 59 | end 60 | scheduledValue.current = nil 61 | end) 62 | 63 | return value, setThrottledValue 64 | end 65 | 66 | return useThrottledState 67 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/useToggle.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useState = React.useState 4 | local useMemo = React.useMemo 5 | 6 | export type Toggle = { 7 | toggle: () -> (), 8 | on: () -> (), 9 | off: () -> (), 10 | } 11 | 12 | local function flipBoolean(previous: boolean): boolean 13 | return not previous 14 | end 15 | 16 | local function useToggle(defaultValue: boolean?): (boolean, Toggle) 17 | local value, setValue = useState(defaultValue == true) 18 | 19 | local toggle = useMemo(function() 20 | return { 21 | toggle = function() 22 | setValue(flipBoolean) 23 | end, 24 | on = function() 25 | setValue(true) 26 | end, 27 | off = function() 28 | setValue(false) 29 | end, 30 | } 31 | end, { setValue }) 32 | 33 | return value, toggle 34 | end 35 | 36 | return useToggle 37 | -------------------------------------------------------------------------------- /packages/react-lua-hooks/src/useUnmount.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useEffect = React.useEffect 4 | local useRef = React.useRef 5 | 6 | local function useUnmount(fn: () -> ()?) 7 | local onUnmount = useRef(nil :: () -> ()?) 8 | 9 | onUnmount.current = fn 10 | 11 | useEffect(function() 12 | return function() 13 | if onUnmount.current then 14 | onUnmount.current() 15 | end 16 | end 17 | end, {}) 18 | end 19 | 20 | return useUnmount 21 | -------------------------------------------------------------------------------- /packages/react-lua-use-constant/.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /scripts/ 4 | /docs/ 5 | 6 | /roblox 7 | /build 8 | 9 | .gitattributes 10 | CHANGELOG.md 11 | 12 | .yarn 13 | 14 | .darklua* 15 | .luau-analyze.json 16 | .luaurc 17 | foreman.toml 18 | selene.toml 19 | selene_definitions.yml 20 | stylua.toml 21 | .styluaignore 22 | 23 | /globalTypes.d.lua 24 | **/sourcemap.json 25 | *.project.json 26 | 27 | **/__tests__ 28 | **/*.test.lua 29 | **/jest.config.lua 30 | 31 | **/*.rbxl 32 | **/*.rbxlx 33 | **/*.rbxl.lock 34 | **/*.rbxlx.lock 35 | **/*.rbxm 36 | **/*.rbxmx 37 | -------------------------------------------------------------------------------- /packages/react-lua-use-constant/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | 5 | * Initial version 6 | -------------------------------------------------------------------------------- /packages/react-lua-use-constant/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![checks](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml/badge.svg)](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml) 4 | ![version](https://img.shields.io/npm/v/@seaofvoices/react-lua-use-constant?label=version) 5 | [![GitHub top language](https://img.shields.io/github/languages/top/seaofvoices/react-lua-hooks)](https://github.com/luau-lang/luau) 6 | ![license](https://img.shields.io/npm/l/@seaofvoices/react-lua-use-constant) 7 | ![npm](https://img.shields.io/npm/dt/@seaofvoices/react-lua-use-constant) 8 | 9 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/seaofvoices) 10 | 11 |
12 | 13 | # react-lua-use-constant 14 | 15 | A [react-lua](https://github.com/jsdotlua/react-lua) hook to create a value once. It is different from [useMemo](https://react.dev/reference/react/useMemo), which can potentially re-calculate its value. 16 | 17 | ## Installation 18 | 19 | Add `react-lua-use-constant` in your dependencies: 20 | 21 | ```bash 22 | yarn add @seaofvoices/react-lua-use-constant 23 | ``` 24 | 25 | Or if you are using `npm`: 26 | 27 | ```bash 28 | npm install @seaofvoices/react-lua-use-constant 29 | ``` 30 | 31 | ## Usage 32 | 33 | This hook takes an initializer function that gets called once to obtain the constant value. 34 | 35 | ```lua 36 | local useConstant = require('@pkg/@seaofvoices/react-lua-use-constant') 37 | 38 | local function Component(props) 39 | local mountTime = useConstant(function() 40 | return os.time() 41 | end) 42 | 43 | return ... 44 | end 45 | ``` 46 | 47 | ## Other Lua Environments Support 48 | 49 | If you would like to use this library on a Lua environment, where it is currently incompatible, open an issue (or comment on an existing one) to request the appropriate modifications. 50 | 51 | The library uses [darklua](https://github.com/seaofvoices/darklua) to process its code. 52 | 53 | ## License 54 | 55 | This project is available under the MIT license. See [LICENSE.txt](../../LICENSE.txt) for details. 56 | -------------------------------------------------------------------------------- /packages/react-lua-use-constant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seaofvoices/react-lua-use-constant", 3 | "description": "A React hook to create a value once. Prefer using this hook from @seaofvoices/react-lua-hooks", 4 | "version": "1.0.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/seaofvoices/react-lua-hooks.git", 8 | "directory": "packages/react-lua-use-constant" 9 | }, 10 | "keywords": [ 11 | "lua", 12 | "luau", 13 | "react", 14 | "hook" 15 | ], 16 | "license": "MIT", 17 | "main": "./src/init.lua", 18 | "dependencies": { 19 | "@jsdotlua/react": "^17.1.0" 20 | }, 21 | "devDependencies": { 22 | "npmluau": "^0.1.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-lua-use-constant/src/init.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local function useConstant(create: () -> T): T 4 | local ref = React.useRef(nil :: any) 5 | 6 | if not ref.current then 7 | ref.current = { value = create() } 8 | end 9 | 10 | return ref.current.value 11 | end 12 | 13 | return useConstant 14 | -------------------------------------------------------------------------------- /packages/react-render-hook/.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /scripts/ 4 | /docs/ 5 | 6 | /roblox 7 | /build 8 | 9 | .gitattributes 10 | CHANGELOG.md 11 | 12 | .yarn 13 | 14 | .darklua* 15 | .luau-analyze.json 16 | .luaurc 17 | foreman.toml 18 | selene.toml 19 | selene_definitions.yml 20 | stylua.toml 21 | .styluaignore 22 | 23 | /globalTypes.d.lua 24 | **/sourcemap.json 25 | *.project.json 26 | 27 | **/__tests__ 28 | **/*.test.lua 29 | **/jest.config.lua 30 | 31 | **/*.rbxl 32 | **/*.rbxlx 33 | **/*.rbxl.lock 34 | **/*.rbxlx.lock 35 | **/*.rbxm 36 | **/*.rbxmx 37 | -------------------------------------------------------------------------------- /packages/react-render-hook/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.0 4 | 5 | * Initial version 6 | -------------------------------------------------------------------------------- /packages/react-render-hook/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![checks](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml/badge.svg)](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml) 4 | ![version](https://img.shields.io/npm/v/@seaofvoices/react-render-hook?label=version) 5 | [![GitHub top language](https://img.shields.io/github/languages/top/seaofvoices/react-lua-hooks)](https://github.com/luau-lang/luau) 6 | ![license](https://img.shields.io/npm/l/@seaofvoices/react-render-hook) 7 | ![npm](https://img.shields.io/npm/dt/@seaofvoices/react-render-hook) 8 | 9 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/seaofvoices) 10 | 11 |
12 | 13 | # react-render-hook 14 | 15 | A small utility function to quickly test React hooks. 16 | 17 | ## Installation 18 | 19 | Add `react-render-hook` in your dependencies: 20 | 21 | ```bash 22 | yarn add @seaofvoices/react-render-hook 23 | ``` 24 | 25 | Or if you are using `npm`: 26 | 27 | ```bash 28 | npm install @seaofvoices/react-render-hook 29 | ``` 30 | 31 | ## Content 32 | 33 | - [renderHook](#renderhook) 34 | - [createRenderHook](#createrenderhook) 35 | 36 | ## renderHook 37 | 38 | ```lua 39 | function renderHook(hook, ...): { 40 | result: Ref, 41 | rerender: (...) -> (), 42 | unmount: () -> () 43 | } 44 | ``` 45 | 46 | A function that takes a hook and its initial arguments and returns a `result` ref, a function to re-render and a function to unmount. 47 | 48 | In a [jest](https://github.com/jsdotlua/jest-lua) test, it looks like this: 49 | 50 | ```lua 51 | local function useCustomHook(input: string): string 52 | return input .. " " .. input 53 | end 54 | 55 | it('returns the initial value after the first re-render', function() 56 | local renderResult = renderHook(useCustomHook, 'text') 57 | 58 | expect(renderResult.result.current).toBe("text text") 59 | 60 | renderResult.rerender('bye') 61 | 62 | expect(renderResult.result.current).toBe('bye bye') 63 | end) 64 | ``` 65 | 66 | **Note about multiple returned values:** if the hook being rendered returns more than one value, those will be packed with `table.pack`. This means that the `renderResult.result.current` will contain an array. 67 | 68 | ### createRenderHook 69 | 70 | If needed, you can pass custom [`renderOptions`](https://testing-library.com/docs/react-testing-library/api#render-options) to the `createRenderHook`. 71 | 72 | Returns a new `renderHook` function. 73 | 74 | ## License 75 | 76 | This project is available under the MIT license. See [LICENSE.txt](LICENSE.txt) for details. 77 | -------------------------------------------------------------------------------- /packages/react-render-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seaofvoices/react-render-hook", 3 | "description": "A test helper to render React hooks using react-testing-library", 4 | "version": "0.1.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/seaofvoices/react-lua-hooks.git", 8 | "directory": "packages/react-render-hook" 9 | }, 10 | "keywords": [ 11 | "lua", 12 | "luau", 13 | "react", 14 | "roblox", 15 | "hook" 16 | ], 17 | "license": "MIT", 18 | "main": "./src/init.lua", 19 | "dependencies": { 20 | "@jsdotlua/react": "^17.1.0", 21 | "@jsdotlua/react-testing-library": "^12.2.1-rc.1" 22 | }, 23 | "devDependencies": { 24 | "npmluau": "^0.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/react-render-hook/src/createRenderHook.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | local ReactTesting = require('@pkg/@jsdotlua/react-testing-library') 3 | 4 | local function createRenderHook(renderOptions: { [string]: any }?) 5 | local function renderHook(renderCallback, ...) 6 | local args = table.pack(...) 7 | 8 | local result = React.createRef() 9 | 10 | local function TestComponent(props: { renderCallbackProps: { [number]: any, n: number } }) 11 | local renderCallbackProps = props.renderCallbackProps 12 | local pendingResult = table.pack( 13 | renderCallback(table.unpack(renderCallbackProps, 1, renderCallbackProps.n)) 14 | ) 15 | 16 | React.useEffect(function() 17 | result.current = if pendingResult.n > 1 18 | then pendingResult 19 | else table.unpack(pendingResult, 1, 1) 20 | end) 21 | 22 | return nil 23 | end 24 | 25 | local renderResult = ReactTesting.render( 26 | React.createElement(TestComponent, { renderCallbackProps = args }), 27 | renderOptions or {} 28 | ) 29 | local baseRerender = renderResult.rerender 30 | local unmount = renderResult.unmount 31 | 32 | local function rerender(...) 33 | local args = table.pack(...) 34 | return baseRerender(React.createElement(TestComponent, { renderCallbackProps = args })) 35 | end 36 | 37 | return { result = result, rerender = rerender, unmount = unmount } 38 | end 39 | 40 | return renderHook 41 | end 42 | 43 | return createRenderHook 44 | -------------------------------------------------------------------------------- /packages/react-render-hook/src/init.lua: -------------------------------------------------------------------------------- 1 | local createRenderHook = require('./createRenderHook') 2 | 3 | return { 4 | renderHook = createRenderHook(), 5 | createRenderHook = createRenderHook, 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /scripts/ 4 | /docs/ 5 | 6 | /roblox 7 | /build 8 | 9 | .gitattributes 10 | CHANGELOG.md 11 | 12 | .yarn 13 | 14 | .darklua* 15 | .luau-analyze.json 16 | .luaurc 17 | foreman.toml 18 | selene.toml 19 | selene_definitions.yml 20 | stylua.toml 21 | .styluaignore 22 | 23 | /globalTypes.d.lua 24 | **/sourcemap.json 25 | *.project.json 26 | 27 | **/__tests__ 28 | **/*.test.lua 29 | **/jest.config.lua 30 | 31 | **/*.rbxl 32 | **/*.rbxlx 33 | **/*.rbxl.lock 34 | **/*.rbxlx.lock 35 | **/*.rbxm 36 | **/*.rbxmx 37 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.0 4 | 5 | * Initial version 6 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![checks](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml/badge.svg)](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml) 4 | ![version](https://img.shields.io/npm/v/@seaofvoices/react-roblox-hooks?label=version) 5 | [![GitHub top language](https://img.shields.io/github/languages/top/seaofvoices/react-lua-hooks)](https://github.com/luau-lang/luau) 6 | ![license](https://img.shields.io/npm/l/@seaofvoices/react-roblox-hooks) 7 | ![npm](https://img.shields.io/npm/dt/@seaofvoices/react-roblox-hooks) 8 | 9 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/seaofvoices) 10 | 11 |
12 | 13 | # react-roblox-hooks 14 | 15 | This package is a collection of hooks for [react-lua](https://github.com/jsdotlua/react-lua), specifically target for development on Roblox. 16 | 17 | ## Installation 18 | 19 | Add `react-roblox-hooks` in your dependencies: 20 | 21 | ```bash 22 | yarn add @seaofvoices/react-roblox-hooks 23 | ``` 24 | 25 | Or if you are using `npm`: 26 | 27 | ```bash 28 | npm install @seaofvoices/react-roblox-hooks 29 | ``` 30 | 31 | ## Content 32 | 33 | **Hooks:** 34 | 35 | - [useService](#useservice) 36 | - [useCamera](#usecamera) 37 | - [useCameraCFrame](#usecameracframe) 38 | - [useEvent](#useevent) 39 | - [useGuiObjectSizeBinding](#useguiobjectsizebinding) 40 | - [useLocalPlayer](#uselocalplayer) 41 | - [useObjectLocation](#useobjectlocation) 42 | - [usePropertyChange](#usepropertychange) 43 | - [useTaggedInstances](#usetaggedinstances) 44 | - [useTextSize](#usetextsize) 45 | - [useViewportSize](#useviewportsize) 46 | 47 | **Components:** 48 | 49 | - [ServiceProvider](#serviceprovider) 50 | 51 | ### useService 52 | 53 | ```lua 54 | function useService(className: string): Instance 55 | ``` 56 | 57 | A hook that returns the given service from its class name, as usually done with `game:GetService(className)`. Usefull when testing a component that requires a mock of a given service, which can be provided using the [ServiceProvider](#serviceprovider) 58 | 59 | #### Example 60 | 61 | ```lua 62 | local function Component(props) 63 | local camera = useCamera() -- get the Camera object 64 | 65 | -- ... 66 | end 67 | ``` 68 | 69 | ### useCamera 70 | 71 | ```lua 72 | function useCamera(): Camera 73 | ``` 74 | 75 | A hook that returns the [CurrentCamera](https://create.roblox.com/docs/reference/engine/classes/Workspace#CurrentCamera) value from the Workspace. 76 | 77 | #### Example 78 | 79 | ```lua 80 | local function Component(props) 81 | local camera = useCamera() -- get the Camera object 82 | 83 | -- ... 84 | end 85 | ``` 86 | 87 | ### useCameraCFrame 88 | 89 | ```lua 90 | function useCameraCFrame(fn: (CFrame) -> (), deps: { any }) 91 | ``` 92 | 93 | A hook to subscribe to each camera CFrame changes. 94 | 95 | Changes in the dependency array will disconnect and reconnect with the updated function. 96 | 97 | #### Example 98 | 99 | ```lua 100 | local function Component(props) 101 | local visible, setVisible = useState(false) 102 | 103 | useCameraCFrame(function(cframe: CFrame) 104 | local distance = ... -- compute distance from player 105 | 106 | -- trigger a state update to make something visible 107 | -- when the camera is close enough 108 | setVisible(distance < 30) 109 | end, {}) 110 | 111 | -- ... 112 | end 113 | ``` 114 | 115 | ### useEvent 116 | 117 | A hook to subscribe to Roblox events. Runs a function when the specified event is triggered. 118 | 119 | Changes in the dependency array will disconnect and reconnect with the updated function. 120 | 121 | ```lua 122 | function useEvent( 123 | event: RBXScriptSignal, 124 | fn: (T...) -> (), 125 | deps: { any } 126 | ) 127 | ``` 128 | 129 | #### Example 130 | 131 | ```lua 132 | local function Component(props) 133 | 134 | -- ... 135 | end 136 | ``` 137 | 138 | ### useGuiSizeBinding 139 | 140 | A hook that returns a binding for a GuiObject's [AbsoluteSize](https://create.roblox.com/docs/reference/engine/classes/GuiBase2d#AbsoluteSize) (a `Vector2`). Returns a ref that has to be assigned to a GuiBase2d instance. 141 | 142 | ```lua 143 | function useGuiSizeBinding(): (React.Ref, React.Binding) 144 | ``` 145 | 146 | #### Example 147 | 148 | ```lua 149 | local function Component(props) 150 | local ref, binding = useGuiSizeBinding() 151 | 152 | return React.createElement("Frame", { 153 | -- assign the ref to the frame and the binding will 154 | -- automatically update with the frame AbsoluteSize property 155 | ref = ref, 156 | }) 157 | end 158 | ``` 159 | 160 | ### useLocalPlayer 161 | 162 | A hook that returns the LocalPlayer object. Use this hook to access information and perform actions related to the local player. Note that this hook will only work when used on client-side context scripts. 163 | 164 | ```lua 165 | function useLocalPlayer(): Player 166 | ``` 167 | 168 | #### Example 169 | 170 | ```lua 171 | local function Component(props) 172 | local player = useLocalPlayer(): Player 173 | -- ... 174 | end 175 | ``` 176 | 177 | ### useObjectLocation 178 | 179 | A hook to track the location (CFrame) changes of a given PVInstance (typically a [model](https://create.roblox.com/docs/reference/engine/classes/Model) or [BasePart](https://create.roblox.com/docs/reference/engine/classes/BasePart)). It enables a component to respond to object movements in a game. 180 | 181 | ```lua 182 | function useObjectLocation( 183 | object: PVInstance?, 184 | fn: (CFrame) -> (), 185 | deps: { any } 186 | ) 187 | ``` 188 | 189 | #### Example 190 | 191 | ```lua 192 | local function Component(props) 193 | 194 | -- ... 195 | end 196 | ``` 197 | 198 | ### usePropertyChange 199 | 200 | A hook to subscribe to property changes of a given Instance. Errors if the property does not exist on the Instance. If the given instance if `nil` is simply disconnects the previous connection. 201 | 202 | ```lua 203 | function usePropertyChange( 204 | instance: Instance?, 205 | property: string, 206 | fn: (T) -> (), 207 | deps: { any } 208 | ) 209 | ``` 210 | 211 | #### Example 212 | 213 | ```lua 214 | local function Component(props) 215 | 216 | -- ... 217 | end 218 | ``` 219 | 220 | ### useTaggedInstances 221 | 222 | A hook to retrieve instances in the game with a specified [CollectionService](https://create.roblox.com/docs/reference/engine/classes/CollectionService) tag. It returns an array of instances (or a mapped array based on a mapping function). 223 | 224 | ```lua 225 | function useTaggedInstances(tagName: string): { Instance } 226 | function useTaggedInstances(tagName: string, mapFn: (Instance) -> T?): { T } 227 | ``` 228 | 229 | #### Example 230 | 231 | ```lua 232 | local function Component(props) 233 | 234 | -- ... 235 | end 236 | ``` 237 | 238 | ### useTextSize 239 | 240 | A hook to calculate the size of a given text string based on provided options such as font size and style. It is useful for dynamically adjusting UI elements based on the size of the displayed text. 241 | 242 | ```lua 243 | function useTextSize(text: string, options: Options): Vector2 244 | -- where 245 | type Options = { size: number, font: Font, width: number? } 246 | ``` 247 | 248 | #### Example 249 | 250 | ```lua 251 | local function Component(props) 252 | 253 | -- ... 254 | end 255 | ``` 256 | 257 | ### useViewportSize 258 | 259 | A hook to subscribe to changes in the viewport size. It enables components to respond to screen size changes, allowing for responsive adjustments. 260 | 261 | ```lua 262 | function useViewportSize(fn: (Vector2) -> (), deps: { any }) 263 | ``` 264 | 265 | #### Example 266 | 267 | ```lua 268 | local function Component(props) 269 | 270 | -- ... 271 | end 272 | ``` 273 | 274 | ### ServiceProvider 275 | 276 | A component that can override the default service provider (which simply calls `game:GetService(className)`) with a custom implementation 277 | 278 | #### Example 279 | 280 | ```lua 281 | local function MockServiceProvider(props) 282 | local mocks = props.mocks 283 | local function provideMocks(className: string): Instance 284 | -- return the mocked service or default to the real one 285 | return mocks[className] or game:GetService(className) 286 | end 287 | 288 | return React.createElement(ServiceProvider, { 289 | value = provideMocks 290 | }) 291 | end 292 | ``` 293 | 294 | ## License 295 | 296 | This project is available under the MIT license. See [LICENSE.txt](../../LICENSE.txt) for details. 297 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seaofvoices/react-roblox-hooks", 3 | "description": "A collection of React hooks specific to the Roblox environment", 4 | "version": "0.1.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/seaofvoices/react-lua-hooks.git", 8 | "directory": "packages/react-roblox-hooks" 9 | }, 10 | "keywords": [ 11 | "lua", 12 | "luau", 13 | "react", 14 | "roblox", 15 | "hook" 16 | ], 17 | "license": "MIT", 18 | "main": "./src/init.lua", 19 | "dependencies": { 20 | "@jsdotlua/react": "^17.1.0", 21 | "@seaofvoices/react-lua-use-constant": "workspace:^", 22 | "@seaofvoices/react-roblox-use-service": "workspace:^", 23 | "luau-disk": "^0.1.1" 24 | }, 25 | "devDependencies": { 26 | "npmluau": "^0.1.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/init.lua: -------------------------------------------------------------------------------- 1 | local ReactUseService = require('@pkg/@seaofvoices/react-roblox-use-service') 2 | 3 | local useCamera = require('./useCamera') 4 | local useCameraCFrame = require('./useCameraCFrame') 5 | local useEvent = require('./useEvent') 6 | local useGuiSizeBinding = require('./useGuiSizeBinding') 7 | local useLocalPlayer = require('./useLocalPlayer') 8 | local useObjectLocation = require('./useObjectLocation') 9 | local usePropertyChange = require('./usePropertyChange') 10 | local useTaggedInstances = require('./useTaggedInstances') 11 | local useTextSize = require('./useTextSize') 12 | local useViewportSize = require('./useViewportSize') 13 | 14 | return { 15 | ServiceProvider = ReactUseService.ServiceProvider, 16 | useService = ReactUseService.useService, 17 | 18 | useCamera = useCamera, 19 | useCameraCFrame = useCameraCFrame, 20 | useEvent = useEvent, 21 | useGuiSizeBinding = useGuiSizeBinding, 22 | useLocalPlayer = useLocalPlayer, 23 | useObjectLocation = useObjectLocation, 24 | usePropertyChange = usePropertyChange, 25 | useTaggedInstances = useTaggedInstances, 26 | useTextSize = useTextSize, 27 | useViewportSize = useViewportSize, 28 | } 29 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useCamera.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService 3 | 4 | local useMemo = React.useMemo 5 | 6 | local function useCamera(): Camera 7 | local workspace = useService('Workspace') 8 | 9 | local camera = useMemo(function() 10 | return workspace.CurrentCamera 11 | end, { workspace }) 12 | 13 | return camera 14 | end 15 | 16 | return useCamera 17 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useCameraCFrame.lua: -------------------------------------------------------------------------------- 1 | local useCamera = require('./useCamera') 2 | local usePropertyChange = require('./usePropertyChange') 3 | 4 | local function useCameraCFrame(fn: (CFrame) -> (), deps: { any }) 5 | local camera = useCamera() 6 | 7 | usePropertyChange(camera, 'CFrame', fn, deps) 8 | end 9 | 10 | return useCameraCFrame 11 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useEvent.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useEffect = React.useEffect 4 | local useCallback = React.useCallback 5 | 6 | local function useEvent(event: RBXScriptSignal, fn: (T...) -> (), deps: { any }) 7 | local memoizedFn = useCallback(fn, deps) 8 | 9 | useEffect(function() 10 | local connection = event:Connect(memoizedFn) 11 | return function() 12 | connection:Disconnect() 13 | end 14 | end, { event :: any, memoizedFn }) 15 | end 16 | 17 | return useEvent 18 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useGuiSizeBinding.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useBinding = React.useBinding 4 | local useRef = React.useRef 5 | local useEffect = React.useEffect 6 | 7 | local function useGuiSizeBinding(): (React.Ref, React.Binding) 8 | local ref = useRef(nil :: GuiObject?) 9 | local binding, setBinding = useBinding(Vector2.zero) 10 | 11 | local current = ref.current 12 | 13 | useEffect(function() 14 | if current == nil or not current:IsA('GuiBase2d') then 15 | setBinding(Vector2.zero) 16 | return 17 | end 18 | local current = current :: GuiObject 19 | 20 | local connection = current:GetPropertyChangedSignal('AbsoluteSize'):Connect(function() 21 | setBinding(current.AbsoluteSize) 22 | end) 23 | 24 | return function() 25 | connection:Disconnect() 26 | end 27 | end, { current }) 28 | 29 | return ref, binding 30 | end 31 | 32 | return useGuiSizeBinding 33 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useLocalPlayer.lua: -------------------------------------------------------------------------------- 1 | local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService 2 | 3 | local function useLocalPlayer(): Player 4 | local player = useService('Players').LocalPlayer 5 | 6 | return player 7 | end 8 | 9 | return useLocalPlayer 10 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useObjectLocation.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useCallback = React.useCallback 4 | local useEffect = React.useEffect 5 | 6 | local function useObjectLocation(object: PVInstance?, fn: (CFrame) -> (), deps: { any }) 7 | local memoizedFn = useCallback(fn, deps) 8 | 9 | useEffect(function() 10 | if not object or not object:IsA('PVInstance') then 11 | return 12 | end 13 | local object = object :: PVInstance 14 | 15 | local function onChange() 16 | local pivot = object:GetPivot() 17 | memoizedFn(pivot) 18 | end 19 | 20 | local connection = object.Changed:Connect(onChange) 21 | 22 | onChange() 23 | 24 | return function() 25 | connection:Disconnect() 26 | end 27 | end, { object :: any, memoizedFn }) 28 | end 29 | 30 | return useObjectLocation 31 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/usePropertyChange.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local useEffect = React.useEffect 4 | local useCallback = React.useCallback 5 | 6 | local function usePropertyChange( 7 | instance: Instance?, 8 | property: string, 9 | fn: (T) -> (), 10 | deps: { any } 11 | ) 12 | local memoizedFn = useCallback(fn, deps) 13 | 14 | useEffect(function() 15 | if instance == nil then 16 | return 17 | end 18 | local instance = instance :: Instance 19 | 20 | local connection = instance:GetPropertyChangedSignal(property):Connect(function() 21 | memoizedFn((instance :: any)[property]) 22 | end) 23 | 24 | memoizedFn((instance :: any)[property]) 25 | 26 | return function() 27 | connection:Disconnect() 28 | end 29 | end, { instance or false :: any, property, memoizedFn }) 30 | end 31 | 32 | return usePropertyChange 33 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useTaggedInstances.lua: -------------------------------------------------------------------------------- 1 | local Disk = require('@pkg/luau-disk') 2 | local React = require('@pkg/@jsdotlua/react') 3 | local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService 4 | 5 | local Array = Disk.Array 6 | 7 | local function useTaggedInstances(tagName: string, mapFn: ((Instance) -> T?)?): { T } 8 | local instances, setInstances = React.useState({} :: { Instance }) 9 | local collectionService = useService('CollectionService') 10 | 11 | React.useEffect(function() 12 | local addedConnection = collectionService 13 | :GetInstanceAddedSignal(tagName) 14 | :Connect(function(newInstance: Instance) 15 | setInstances(function(previous) 16 | return Array.push(previous, newInstance) 17 | end) 18 | end) 19 | 20 | local removedConnection = collectionService 21 | :GetInstanceRemovedSignal(tagName) 22 | :Connect(function(removedInstance) 23 | setInstances(function(previous) 24 | local index = table.find(previous, removedInstance) 25 | if index == nil then 26 | return previous 27 | else 28 | local cloned = table.clone(previous) 29 | table.remove(cloned, index) 30 | return cloned 31 | end 32 | end) 33 | end) 34 | 35 | setInstances(collectionService:GetTagged(tagName)) 36 | 37 | return function() 38 | addedConnection:Disconnect() 39 | removedConnection:Disconnect() 40 | end 41 | end, { tagName :: any, collectionService }) 42 | 43 | local values = React.useMemo(function() 44 | if mapFn == nil then 45 | return instances 46 | else 47 | return Array.map(instances, mapFn) 48 | end 49 | end, { instances :: any, mapFn or false }) 50 | 51 | return (values :: any) :: { T } 52 | end 53 | 54 | return (useTaggedInstances :: any) :: ( 55 | ((tagName: string, mapFn: (Instance) -> T?) -> { T }) & ((tagName: string) -> { Instance }) 56 | ) 57 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useTextSize.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService 3 | 4 | local function useTextSize( 5 | text: string, 6 | options: { size: number, font: Font, width: number? } 7 | ): Vector2 8 | local textService = useService('TextService') 9 | 10 | local textSize = options.size 11 | local textFont = options.font 12 | local width = options.width or 0 13 | 14 | local textBoundsParams = React.useMemo(function() 15 | local params = Instance.new('GetTextBoundsParams') 16 | params.Font = textFont 17 | params.Size = textSize 18 | params.Width = width 19 | return params 20 | end, { textFont :: any, textSize, width }) 21 | 22 | local frameSize = React.useMemo(function() 23 | local params = textBoundsParams:Clone() 24 | params.Text = text 25 | return textService:GetTextBoundsAsync(params) 26 | end, { text :: any, textBoundsParams, textService }) 27 | 28 | return frameSize 29 | end 30 | 31 | return useTextSize 32 | -------------------------------------------------------------------------------- /packages/react-roblox-hooks/src/useViewportSize.lua: -------------------------------------------------------------------------------- 1 | local useCamera = require('./useCamera') 2 | local usePropertyChange = require('./usePropertyChange') 3 | 4 | local function useViewportSize(fn: (Vector2) -> (), deps: { any }) 5 | local camera = useCamera() 6 | 7 | usePropertyChange(camera, 'ViewportSize', fn, deps) 8 | end 9 | 10 | return useViewportSize 11 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /scripts/ 4 | /docs/ 5 | 6 | /roblox 7 | /build 8 | 9 | .gitattributes 10 | CHANGELOG.md 11 | 12 | .yarn 13 | 14 | .darklua* 15 | .luau-analyze.json 16 | .luaurc 17 | foreman.toml 18 | selene.toml 19 | selene_definitions.yml 20 | stylua.toml 21 | .styluaignore 22 | 23 | /globalTypes.d.lua 24 | **/sourcemap.json 25 | *.project.json 26 | 27 | **/__tests__ 28 | **/*.test.lua 29 | **/jest.config.lua 30 | 31 | **/*.rbxl 32 | **/*.rbxlx 33 | **/*.rbxl.lock 34 | **/*.rbxlx.lock 35 | **/*.rbxm 36 | **/*.rbxmx 37 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | 5 | * Initial version 6 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![checks](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml/badge.svg)](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml) 4 | ![version](https://img.shields.io/npm/v/@seaofvoices/react-roblox-use-service?label=version) 5 | [![GitHub top language](https://img.shields.io/github/languages/top/seaofvoices/react-lua-hooks)](https://github.com/luau-lang/luau) 6 | ![license](https://img.shields.io/npm/l/@seaofvoices/react-roblox-use-service) 7 | ![npm](https://img.shields.io/npm/dt/@seaofvoices/react-roblox-use-service) 8 | 9 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/seaofvoices) 10 | 11 |
12 | 13 | # react-roblox-use-service 14 | 15 | A Luau library for Roblox that provides a `useService` hook and a `ServiceProvider` component for [react-lua](https://github.com/jsdotlua/react-lua). 16 | 17 | ## Installation 18 | 19 | Add `react-roblox-use-service` in your dependencies: 20 | 21 | ```bash 22 | yarn add @seaofvoices/react-roblox-use-service 23 | ``` 24 | 25 | Or if you are using `npm`: 26 | 27 | ```bash 28 | npm install @seaofvoices/react-roblox-use-service 29 | ``` 30 | 31 | ## Content 32 | 33 | ### useService 34 | 35 | ```lua 36 | function useService(className: string): Instance 37 | ``` 38 | 39 | A hook that returns the given service from its class name, as usually done with `game:GetService(className)`. Usefull when testing a component that requires a mock of a given service, which can be provided using the [ServiceProvider](#serviceprovider) 40 | 41 | #### Example 42 | 43 | ```lua 44 | local ReactRobloxUseService = require('@pkg/@seaofvoices/react-roblox-use-service') 45 | local useService = ReactRobloxUseService.useService 46 | 47 | local function MyButton(props) 48 | local ReplicatedStorage = useService("ReplicatedStorage") 49 | local Selection = useService("Selection") 50 | 51 | return React.createElement(CustomButton, { 52 | onClick = function() 53 | local new = Instance.new("Folder") 54 | new.Parent = ReplicatedStorage 55 | 56 | Selection:Set({ new }) 57 | end, 58 | }) 59 | end 60 | ``` 61 | 62 | ### ServiceProvider 63 | 64 | A component that can override the default service provider (which simply calls `game:GetService(className)`) with a custom implementation. Useful for testing components with a mocked service. 65 | 66 | #### Example 67 | 68 | ```lua 69 | local ReactRobloxUseService = require('@pkg/@seaofvoices/react-roblox-use-service') 70 | local ServiceProvider = ReactRobloxUseService.ServiceProvider 71 | 72 | local function MockServiceProvider(props) 73 | local mocks = props.mocks 74 | local function provideMocks(className: string): Instance 75 | -- return the mocked service or default to the real one 76 | return mocks[className] or game:GetService(className) 77 | end 78 | 79 | return React.createElement(ServiceProvider, { 80 | value = provideMocks 81 | }) 82 | end 83 | ``` 84 | 85 | ## License 86 | 87 | This project is available under the MIT license. See [LICENSE.txt](LICENSE.txt) for details. 88 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seaofvoices/react-roblox-use-service", 3 | "description": "A React hook to easily obtain and mock Roblox services", 4 | "version": "1.0.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/seaofvoices/react-lua-hooks.git", 8 | "directory": "packages/react-roblox-use-service" 9 | }, 10 | "keywords": [ 11 | "lua", 12 | "luau", 13 | "react", 14 | "roblox", 15 | "hook" 16 | ], 17 | "license": "MIT", 18 | "main": "./src/init.lua", 19 | "dependencies": { 20 | "@jsdotlua/react": "^17.1.0" 21 | }, 22 | "devDependencies": { 23 | "@jsdotlua/jest-globals": "^3.6.1-rc.2", 24 | "@seaofvoices/react-render-hook": "workspace:^", 25 | "npmluau": "^0.1.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/src/ServiceProviderContext.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local function defaultServiceProvider(className: string): Instance 4 | return game:GetService(className) 5 | end 6 | 7 | local ServiceProviderContext: React.Context<(className: string) -> Instance> = 8 | React.createContext(defaultServiceProvider) 9 | ServiceProviderContext.displayName = 'ServiceProviderContext' 10 | 11 | return ServiceProviderContext 12 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/src/__tests__/useService.test.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | local jestGlobals = require('@pkg/@jsdotlua/jest-globals') 3 | local renderHook = require('@pkg/@seaofvoices/react-render-hook') 4 | 5 | local ServiceProviderContext = require('../ServiceProviderContext') 6 | local useService = require('../useService') 7 | 8 | local expect = jestGlobals.expect 9 | local it = jestGlobals.it 10 | local beforeEach = jestGlobals.beforeEach 11 | 12 | local services 13 | local renderWithServices 14 | 15 | beforeEach(function() 16 | services = {} 17 | renderWithServices = renderHook.createRenderHook({ 18 | wrapper = function(props) 19 | return React.createElement(ServiceProviderContext.Provider, { 20 | value = function(name) 21 | return services[name] 22 | end, 23 | }, props.children) 24 | end, 25 | }) 26 | end) 27 | 28 | it('returns the RunService', function() 29 | local runServiceMock = {} 30 | 31 | services.RunService = runServiceMock 32 | local renderResult = renderWithServices(useService :: any, 'RunService') 33 | 34 | expect(renderResult.result.current).toBe(runServiceMock) 35 | end) 36 | 37 | it('returns a different service after re-rendering', function() 38 | local runServiceMock = {} 39 | local workspaceMock = {} 40 | 41 | services.RunService = runServiceMock 42 | services.Workspace = workspaceMock 43 | local renderResult = renderWithServices(useService :: any, 'RunService') 44 | 45 | expect(renderResult.result.current).toBe(runServiceMock) 46 | 47 | renderResult.rerender('Workspace') 48 | 49 | expect(renderResult.result.current).toBe(workspaceMock) 50 | end) 51 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/src/init.lua: -------------------------------------------------------------------------------- 1 | local ServiceProviderContext = require('./ServiceProviderContext') 2 | local useService = require('./useService') 3 | 4 | return { 5 | useService = useService, 6 | ServiceProvider = ServiceProviderContext.Provider, 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-roblox-use-service/src/useService.lua: -------------------------------------------------------------------------------- 1 | local React = require('@pkg/@jsdotlua/react') 2 | 3 | local ServiceProviderContext = require('./ServiceProviderContext') 4 | 5 | local function useService(className: string): Instance 6 | local serviceProvider = React.useContext(ServiceProviderContext) 7 | 8 | return serviceProvider(className) 9 | end 10 | 11 | type useServiceFn = 12 | (('RunService') -> RunService) 13 | & (('ReplicatedFirst') -> ReplicatedFirst) 14 | & (('ReplicatedStorage') -> ReplicatedStorage) 15 | & (('CollectionService') -> CollectionService) 16 | & (('Players') -> Players) 17 | & (('TweenService') -> TweenService) 18 | & (('ContextActionService') -> ContextActionService) 19 | & (('MarketplaceService') -> MarketplaceService) 20 | & (('Lighting') -> Lighting) 21 | & (('TextService') -> TextService) 22 | & (('TextChatService') -> TextChatService) 23 | & (('HttpService') -> HttpService) 24 | & (('PhysicsService') -> PhysicsService) 25 | & (('Workspace') -> Workspace) 26 | & (('UserInputService') -> UserInputService) 27 | & ((className: string) -> Instance) 28 | 29 | return (useService :: any) :: useServiceFn 30 | -------------------------------------------------------------------------------- /roblox-test.server.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService('ReplicatedStorage') 2 | 3 | local jest = require('@pkg/@jsdotlua/jest') 4 | 5 | local success, result = jest.runCLI(ReplicatedStorage, { 6 | color = false, 7 | colors = false, 8 | }, { ReplicatedStorage:FindFirstChild('node_modules'):FindFirstChild('@seaofvoices') }):await() 9 | 10 | if not success then 11 | error(result) 12 | end 13 | 14 | task.wait(0.5) 15 | -------------------------------------------------------------------------------- /scripts/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | TYPES_FILE=globalTypes.d.lua 6 | 7 | if [ ! -f "$TYPES_FILE" ]; then 8 | curl https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.lua > $TYPES_FILE 9 | fi 10 | 11 | luau-lsp analyze --base-luaurc=.luaurc --settings=.luau-analyze.json \ 12 | --ignore '**/node_modules/**' --ignore 'node_modules/**' \ 13 | --definitions=$TYPES_FILE \ 14 | packages 15 | -------------------------------------------------------------------------------- /scripts/roblox-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ ! -d node_modules ]; then 6 | rm -rf roblox 7 | yarn install 8 | fi 9 | 10 | if [ -d "roblox" ]; then 11 | ls -d roblox/* | grep -v node_modules | xargs rm -rf 12 | fi 13 | 14 | rojo sourcemap test-place.project.json -o sourcemap.json 15 | 16 | darklua process jest.config.lua roblox/jest.config.lua 17 | darklua process roblox-test.server.lua roblox/roblox-test.server.lua 18 | darklua process node_modules roblox/node_modules 19 | 20 | cp test-place.project.json roblox/ 21 | 22 | rojo build roblox/test-place.project.json -o place.rbxl 23 | 24 | run-in-roblox --place place.rbxl --script roblox/roblox-test.server.lua 25 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "selene_definitions" 2 | 3 | [rules] 4 | global_usage = "allow" 5 | shadowing = "allow" 6 | 7 | [config] 8 | empty_if = { comments_count = true } 9 | -------------------------------------------------------------------------------- /selene_definitions.yml: -------------------------------------------------------------------------------- 1 | base: roblox 2 | name: selene_defs 3 | globals: 4 | # override Roblox require style with string requires 5 | require: 6 | args: 7 | - type: string 8 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 100 2 | quote_style = "AutoPreferSingle" 3 | indent_type = "Spaces" 4 | line_endings = "Unix" 5 | indent_width = 4 6 | 7 | [sort_requires] 8 | enabled = true 9 | -------------------------------------------------------------------------------- /test-place.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-place", 3 | "tree": { 4 | "$className": "DataModel", 5 | "HttpService": { 6 | "$properties": { 7 | "HttpEnabled": true 8 | } 9 | }, 10 | "Players": { 11 | "$properties": { 12 | "CharacterAutoLoads": false 13 | } 14 | }, 15 | "ReplicatedStorage": { 16 | "node_modules": { 17 | "$path": "./node_modules", 18 | "jest.config": { 19 | "$path": "./jest.config.lua" 20 | } 21 | } 22 | }, 23 | "ServerScriptService": { 24 | "RunTests": { 25 | "$path": "./roblox-test.server.lua" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10c0 7 | 8 | "@jsdotlua/boolean@npm:^1.2.7": 9 | version: 1.2.7 10 | resolution: "@jsdotlua/boolean@npm:1.2.7" 11 | dependencies: 12 | "@jsdotlua/number": "npm:^1.2.7" 13 | checksum: a7342b8a5c5764a99446b91cf91e0e41ec789f468acdb5e657d33eb3c6c207b9c94590d318496466520cb8590b7f4c1baea0aca89798c7f88749f8d237e15c2f 14 | languageName: node 15 | linkType: hard 16 | 17 | "@jsdotlua/chalk@npm:^0.2.1": 18 | version: 0.2.1 19 | resolution: "@jsdotlua/chalk@npm:0.2.1" 20 | checksum: 4db0d7de47dae83daa8be4cb4acbb8cc54e81aa598a686529bd95e95900f365edab65fc354c6114be942cfc8c2f3efc47b78aebc528cd2079e0c67f7e4aeba8d 21 | languageName: node 22 | linkType: hard 23 | 24 | "@jsdotlua/collections@npm:^1.2.7": 25 | version: 1.2.7 26 | resolution: "@jsdotlua/collections@npm:1.2.7" 27 | dependencies: 28 | "@jsdotlua/es7-types": "npm:^1.2.7" 29 | "@jsdotlua/instance-of": "npm:^1.2.7" 30 | checksum: ee4dc2129bdfb2c07a81027738335292f5a37329bd6b34d2be11589384fc6d37d0cdb2bf198d794ad764b188b2441c49d21eea2eedcbf27c8214f4a432164122 31 | languageName: node 32 | linkType: hard 33 | 34 | "@jsdotlua/console@npm:^1.2.7": 35 | version: 1.2.7 36 | resolution: "@jsdotlua/console@npm:1.2.7" 37 | dependencies: 38 | "@jsdotlua/collections": "npm:^1.2.7" 39 | checksum: 0ea98ef96d23a78fadbcf365fe4f0fd6fc04c753305ed180550396ebb7084120c9224d18b323cea35f9261de94c6b29855cadb1d91c1961f3cdc888c4a319413 40 | languageName: node 41 | linkType: hard 42 | 43 | "@jsdotlua/diff-sequences@npm:^3.6.1-rc.2": 44 | version: 3.6.1-rc.2 45 | resolution: "@jsdotlua/diff-sequences@npm:3.6.1-rc.2" 46 | dependencies: 47 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 48 | checksum: 8131dfffd336cfd7a2d38d974145156163c85c13bfc5dc4d36cb9d3735ffaa977751782dfca2d119dccc00badbf1120eef2d399586a5748329da7137030dda01 49 | languageName: node 50 | linkType: hard 51 | 52 | "@jsdotlua/dom-testing-library@npm:^8.14.2-rc.1": 53 | version: 8.14.2-rc.1 54 | resolution: "@jsdotlua/dom-testing-library@npm:8.14.2-rc.1" 55 | dependencies: 56 | "@jsdotlua/chalk": "npm:^0.2.1" 57 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 58 | "@jsdotlua/jest-globals": "npm:^3.6.1-rc.2" 59 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 60 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 61 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 62 | "@jsdotlua/promise": "npm:^3.5.0" 63 | luau-regexp: "npm:^0.2.1" 64 | checksum: a9f7cd869e0cb543ca8d005cfc4ef654746d843229e1e9e21f98315b7046df02aea2a75a612f0d47cac507b20a80c5e1f6502fea7c4a6b761bcddd958f107dc4 65 | languageName: node 66 | linkType: hard 67 | 68 | "@jsdotlua/emittery@npm:^3.6.1-rc.2": 69 | version: 3.6.1-rc.2 70 | resolution: "@jsdotlua/emittery@npm:3.6.1-rc.2" 71 | dependencies: 72 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 73 | "@jsdotlua/promise": "npm:^3.5.0" 74 | checksum: b7093886298b74837a4fa265c197419579cab06e2d9353ce752f5013bd8a05e29be35865a6ef66258ea2d45af046c7c98e24520489ba54729b5ea682c22a8466 75 | languageName: node 76 | linkType: hard 77 | 78 | "@jsdotlua/es7-types@npm:^1.2.7": 79 | version: 1.2.7 80 | resolution: "@jsdotlua/es7-types@npm:1.2.7" 81 | checksum: c639dfb8b6039a385424e24b6636cc12607ba1ff09da046ec37bd1af47b55a95f7a4b3ae8f4bdbd2f0b7bed08316e00e32debbc11afc0acf252182afe06a8f71 82 | languageName: node 83 | linkType: hard 84 | 85 | "@jsdotlua/expect@npm:^3.6.1-rc.2": 86 | version: 3.6.1-rc.2 87 | resolution: "@jsdotlua/expect@npm:3.6.1-rc.2" 88 | dependencies: 89 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 90 | "@jsdotlua/jest-matcher-utils": "npm:^3.6.1-rc.2" 91 | "@jsdotlua/jest-message-util": "npm:^3.6.1-rc.2" 92 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 93 | "@jsdotlua/jest-snapshot": "npm:^3.6.1-rc.2" 94 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 95 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 96 | "@jsdotlua/promise": "npm:^3.5.0" 97 | luau-regexp: "npm:^0.2.1" 98 | checksum: 7733242b925f053095321427bf082a387c4d2220ea6f64830d8849837792c362cc7dc81ef8563905733aa4811ffb1dc6764f3093dc5bc2eb5a7850e53e8e5a84 99 | languageName: node 100 | linkType: hard 101 | 102 | "@jsdotlua/instance-of@npm:^1.2.7": 103 | version: 1.2.7 104 | resolution: "@jsdotlua/instance-of@npm:1.2.7" 105 | checksum: a3ebb3fd7750fb23afb26338eaa0e4c0dfe7badfd9180b4a72749a0d910269d484ce93c53bdb3219318576a87c175932bbe87466dbff870141b66541c5943eda 106 | languageName: node 107 | linkType: hard 108 | 109 | "@jsdotlua/jest-circus@npm:^3.6.1-rc.2": 110 | version: 3.6.1-rc.2 111 | resolution: "@jsdotlua/jest-circus@npm:3.6.1-rc.2" 112 | dependencies: 113 | "@jsdotlua/chalk": "npm:^0.2.1" 114 | "@jsdotlua/expect": "npm:^3.6.1-rc.2" 115 | "@jsdotlua/jest-each": "npm:^3.6.1-rc.2" 116 | "@jsdotlua/jest-environment": "npm:^3.6.1-rc.2" 117 | "@jsdotlua/jest-matcher-utils": "npm:^3.6.1-rc.2" 118 | "@jsdotlua/jest-message-util": "npm:^3.6.1-rc.2" 119 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 120 | "@jsdotlua/jest-runtime": "npm:^3.6.1-rc.2" 121 | "@jsdotlua/jest-snapshot": "npm:^3.6.1-rc.2" 122 | "@jsdotlua/jest-test-result": "npm:^3.6.1-rc.2" 123 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 124 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 125 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 126 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 127 | "@jsdotlua/promise": "npm:^3.5.0" 128 | "@jsdotlua/throat": "npm:^3.6.1-rc.2" 129 | luau-regexp: "npm:^0.2.1" 130 | checksum: 9afc7a29c871fd5e6ebf330dd9e428e3570b4d9581e248c24edae93a1e54fa917e41173cbd7672c1fea8fd6d7e4ee06b23fb8199bd5f87e726b50fd4d652fe49 131 | languageName: node 132 | linkType: hard 133 | 134 | "@jsdotlua/jest-config@npm:^3.6.1-rc.2": 135 | version: 3.6.1-rc.2 136 | resolution: "@jsdotlua/jest-config@npm:3.6.1-rc.2" 137 | dependencies: 138 | "@jsdotlua/chalk": "npm:^0.2.1" 139 | "@jsdotlua/jest-each": "npm:^3.6.1-rc.2" 140 | "@jsdotlua/jest-environment-roblox": "npm:^3.6.1-rc.2" 141 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 142 | "@jsdotlua/jest-message-util": "npm:^3.6.1-rc.2" 143 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 144 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 145 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 146 | "@jsdotlua/jest-validate": "npm:^3.6.1-rc.2" 147 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 148 | "@jsdotlua/promise": "npm:^3.5.0" 149 | luau-regexp: "npm:^0.2.1" 150 | checksum: 91cdb06da5681c05328cd1e0796c98b0685a7bc766fc5d3190718a79d6755b717598aee8189511947629e903568776e66fffc01bf4b2436c7df2a4fc611ee359 151 | languageName: node 152 | linkType: hard 153 | 154 | "@jsdotlua/jest-console@npm:^3.6.1-rc.2": 155 | version: 3.6.1-rc.2 156 | resolution: "@jsdotlua/jest-console@npm:3.6.1-rc.2" 157 | dependencies: 158 | "@jsdotlua/chalk": "npm:^0.2.1" 159 | "@jsdotlua/jest-each": "npm:^3.6.1-rc.2" 160 | "@jsdotlua/jest-message-util": "npm:^3.6.1-rc.2" 161 | "@jsdotlua/jest-mock": "npm:^3.6.1-rc.2" 162 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 163 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 164 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 165 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 166 | checksum: b3430756752f1b9df7682fe1bb8c4343e4638ebc0cff0251276049138232045f20f06ef2f3a029758c9d4c40c001b70904b80ebb798c5d251a2fabd90d5866a4 167 | languageName: node 168 | linkType: hard 169 | 170 | "@jsdotlua/jest-core@npm:^3.6.1-rc.2": 171 | version: 3.6.1-rc.2 172 | resolution: "@jsdotlua/jest-core@npm:3.6.1-rc.2" 173 | dependencies: 174 | "@jsdotlua/chalk": "npm:^0.2.1" 175 | "@jsdotlua/emittery": "npm:^3.6.1-rc.2" 176 | "@jsdotlua/jest-config": "npm:^3.6.1-rc.2" 177 | "@jsdotlua/jest-console": "npm:^3.6.1-rc.2" 178 | "@jsdotlua/jest-message-util": "npm:^3.6.1-rc.2" 179 | "@jsdotlua/jest-reporters": "npm:^3.6.1-rc.2" 180 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 181 | "@jsdotlua/jest-runner": "npm:^3.6.1-rc.2" 182 | "@jsdotlua/jest-runtime": "npm:^3.6.1-rc.2" 183 | "@jsdotlua/jest-snapshot": "npm:^3.6.1-rc.2" 184 | "@jsdotlua/jest-test-result": "npm:^3.6.1-rc.2" 185 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 186 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 187 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 188 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 189 | "@jsdotlua/promise": "npm:^3.5.0" 190 | luau-regexp: "npm:^0.2.1" 191 | checksum: 8294c3bd8328445ebced024d0094f11e427ffdcc3f4e19b4b0728d963ff44864edf90572d53d2f0d922554b72282439df76e2e74ce8357b98607c51483440c90 192 | languageName: node 193 | linkType: hard 194 | 195 | "@jsdotlua/jest-diff@npm:^3.6.1-rc.2": 196 | version: 3.6.1-rc.2 197 | resolution: "@jsdotlua/jest-diff@npm:3.6.1-rc.2" 198 | dependencies: 199 | "@jsdotlua/chalk": "npm:^0.2.1" 200 | "@jsdotlua/diff-sequences": "npm:^3.6.1-rc.2" 201 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 202 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 203 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 204 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 205 | checksum: e7bd52319e178212ca696bd3c47c3ced5251bf4e62ac57a0e4bfdda79c648c2ce815da34b0423ddcbe0fc260bc595ee5ea4ac1300e1052c8a60e0d3d23217ac5 206 | languageName: node 207 | linkType: hard 208 | 209 | "@jsdotlua/jest-each@npm:^3.6.1-rc.2": 210 | version: 3.6.1-rc.2 211 | resolution: "@jsdotlua/jest-each@npm:3.6.1-rc.2" 212 | dependencies: 213 | "@jsdotlua/chalk": "npm:^0.2.1" 214 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 215 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 216 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 217 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 218 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 219 | luau-regexp: "npm:^0.2.1" 220 | checksum: 91d37f0a6e2fb291f229aabc5f7b0e839827e8751f3fd33a4754ffda51b325d9dd814d518753cb47c8dcad3ae249e3b881d0ffd0aa21a6af74afdb27a67a779d 221 | languageName: node 222 | linkType: hard 223 | 224 | "@jsdotlua/jest-environment-roblox@npm:^3.6.1-rc.2": 225 | version: 3.6.1-rc.2 226 | resolution: "@jsdotlua/jest-environment-roblox@npm:3.6.1-rc.2" 227 | dependencies: 228 | "@jsdotlua/jest-environment": "npm:^3.6.1-rc.2" 229 | "@jsdotlua/jest-fake-timers": "npm:^3.6.1-rc.2" 230 | "@jsdotlua/jest-mock": "npm:^3.6.1-rc.2" 231 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 232 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 233 | "@jsdotlua/promise": "npm:^3.5.0" 234 | checksum: 0ad8b4fb6a140577091d188c4c441109532b638054380c6cd102276fcd835c771d807d6017d72aaa486ca390294adbc42afa6d8471c3ea826bec7914668bfe06 235 | languageName: node 236 | linkType: hard 237 | 238 | "@jsdotlua/jest-environment@npm:^3.6.1-rc.2": 239 | version: 3.6.1-rc.2 240 | resolution: "@jsdotlua/jest-environment@npm:3.6.1-rc.2" 241 | dependencies: 242 | "@jsdotlua/jest-fake-timers": "npm:^3.6.1-rc.2" 243 | "@jsdotlua/jest-mock": "npm:^3.6.1-rc.2" 244 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 245 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 246 | checksum: 9174d88955fd68380bae0118e8600547c62816d006ba070a7397a7d0cd70a88403e3d7c1407d5da1da0e5b6aa8799ca12e4c2ec92ec9796de29201a4c9e4acba 247 | languageName: node 248 | linkType: hard 249 | 250 | "@jsdotlua/jest-fake-timers@npm:^3.6.1-rc.2": 251 | version: 3.6.1-rc.2 252 | resolution: "@jsdotlua/jest-fake-timers@npm:3.6.1-rc.2" 253 | dependencies: 254 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 255 | "@jsdotlua/jest-mock": "npm:^3.6.1-rc.2" 256 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 257 | checksum: 5650e726c80be8fe04fa1b6bcd2a3aee66b146ebbccd102b1c5416516bee5dff284e13f098aba163e01672a82f4cd6a3254d0fb208b65dd9529258b437a8c43e 258 | languageName: node 259 | linkType: hard 260 | 261 | "@jsdotlua/jest-get-type@npm:^3.6.1-rc.2": 262 | version: 3.6.1-rc.2 263 | resolution: "@jsdotlua/jest-get-type@npm:3.6.1-rc.2" 264 | dependencies: 265 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 266 | luau-regexp: "npm:^0.2.1" 267 | checksum: 6216f048dc17275a321ff1f8c96788ec3bab39bb9c8894b216a2419fe7e468448e970a820751a2b64b2a60bb8ebbd5105425927a9895332c234744203659419f 268 | languageName: node 269 | linkType: hard 270 | 271 | "@jsdotlua/jest-globals@npm:^3.6.1-rc.2": 272 | version: 3.6.1-rc.2 273 | resolution: "@jsdotlua/jest-globals@npm:3.6.1-rc.2" 274 | dependencies: 275 | "@jsdotlua/expect": "npm:^3.6.1-rc.2" 276 | "@jsdotlua/jest-environment": "npm:^3.6.1-rc.2" 277 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 278 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 279 | checksum: d9324045c7adc930db71f586e7c886ec9b3165978b271e93d07ee0537dcb22126a414ed2efbf6b62f6aa1f7cedd687ce5b3d6f5ad55ff0470b6562421dee658d 280 | languageName: node 281 | linkType: hard 282 | 283 | "@jsdotlua/jest-matcher-utils@npm:^3.6.1-rc.2": 284 | version: 3.6.1-rc.2 285 | resolution: "@jsdotlua/jest-matcher-utils@npm:3.6.1-rc.2" 286 | dependencies: 287 | "@jsdotlua/chalk": "npm:^0.2.1" 288 | "@jsdotlua/jest-diff": "npm:^3.6.1-rc.2" 289 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 290 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 291 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 292 | luau-regexp: "npm:^0.2.1" 293 | checksum: 156c6fa299c10eb1bda2f1e15f4a9de8e1f9a3a0d8982a012b7e51c17dbd3a668c882bdfb07a6910f19018cb6610700289b83736d2a8ff6c9296d50777ab7081 294 | languageName: node 295 | linkType: hard 296 | 297 | "@jsdotlua/jest-message-util@npm:^3.6.1-rc.2": 298 | version: 3.6.1-rc.2 299 | resolution: "@jsdotlua/jest-message-util@npm:3.6.1-rc.2" 300 | dependencies: 301 | "@jsdotlua/chalk": "npm:^0.2.1" 302 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 303 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 304 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 305 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 306 | luau-regexp: "npm:^0.2.1" 307 | checksum: 48d942b4de4f6b904851e7f2e74d41a211028209d2825cef8061f14d9d0c7c760f2de62037f1b5e7c84540919c30ddd4ae333a4b1d3f72bacf28fdf7ea9c1c59 308 | languageName: node 309 | linkType: hard 310 | 311 | "@jsdotlua/jest-mock@npm:^3.6.1-rc.2": 312 | version: 3.6.1-rc.2 313 | resolution: "@jsdotlua/jest-mock@npm:3.6.1-rc.2" 314 | dependencies: 315 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 316 | checksum: e77f12ac35925cb79ac7831fcf332ff3bd5dbafb016d53d77c0efa16098732a2b14f3a5b34b2be20d903ce683773dbd39ba57cde4bad7130d9fafd0931586625 317 | languageName: node 318 | linkType: hard 319 | 320 | "@jsdotlua/jest-reporters@npm:^3.6.1-rc.2": 321 | version: 3.6.1-rc.2 322 | resolution: "@jsdotlua/jest-reporters@npm:3.6.1-rc.2" 323 | dependencies: 324 | "@jsdotlua/chalk": "npm:^0.2.1" 325 | "@jsdotlua/jest-console": "npm:^3.6.1-rc.2" 326 | "@jsdotlua/jest-message-util": "npm:^3.6.1-rc.2" 327 | "@jsdotlua/jest-mock": "npm:^3.6.1-rc.2" 328 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 329 | "@jsdotlua/jest-test-result": "npm:^3.6.1-rc.2" 330 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 331 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 332 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 333 | "@jsdotlua/path": "npm:^3.6.1-rc.2" 334 | checksum: 90e0d034084085ed873bd10b1a5742d08b0807da1bcfe380043e7dbdfcc3f4ad0048472c65ca73a13c40d182baa5e6596dea0ec6c2898df99a413b561864ea3d 335 | languageName: node 336 | linkType: hard 337 | 338 | "@jsdotlua/jest-roblox-shared@npm:^3.6.1-rc.2": 339 | version: 3.6.1-rc.2 340 | resolution: "@jsdotlua/jest-roblox-shared@npm:3.6.1-rc.2" 341 | dependencies: 342 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 343 | "@jsdotlua/jest-mock": "npm:^3.6.1-rc.2" 344 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 345 | checksum: 61c13286784a247ec8a0f17bd4f6a7740bd1f41277569e5dfad60072f6cdf40d5d163b67d0cbd5d1d0ca4a1c705a4eaa09e87a624709ff494c0b2c4e7af374ff 346 | languageName: node 347 | linkType: hard 348 | 349 | "@jsdotlua/jest-runner@npm:^3.6.1-rc.2": 350 | version: 3.6.1-rc.2 351 | resolution: "@jsdotlua/jest-runner@npm:3.6.1-rc.2" 352 | dependencies: 353 | "@jsdotlua/chalk": "npm:^0.2.1" 354 | "@jsdotlua/emittery": "npm:^3.6.1-rc.2" 355 | "@jsdotlua/jest-circus": "npm:^3.6.1-rc.2" 356 | "@jsdotlua/jest-console": "npm:^3.6.1-rc.2" 357 | "@jsdotlua/jest-environment": "npm:^3.6.1-rc.2" 358 | "@jsdotlua/jest-message-util": "npm:^3.6.1-rc.2" 359 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 360 | "@jsdotlua/jest-runtime": "npm:^3.6.1-rc.2" 361 | "@jsdotlua/jest-test-result": "npm:^3.6.1-rc.2" 362 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 363 | "@jsdotlua/jest-util": "npm:^3.6.1-rc.2" 364 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 365 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 366 | "@jsdotlua/promise": "npm:^3.5.0" 367 | "@jsdotlua/throat": "npm:^3.6.1-rc.2" 368 | checksum: 3615ea2cd7056731911bfea5d82f2e133fd6fb93676867d0850ccbb1cb23d1720fddbcd7e3436d3229fc159d592d32892472b8ce622c06e677abf08d57cc2c1a 369 | languageName: node 370 | linkType: hard 371 | 372 | "@jsdotlua/jest-runtime@npm:^3.6.1-rc.2": 373 | version: 3.6.1-rc.2 374 | resolution: "@jsdotlua/jest-runtime@npm:3.6.1-rc.2" 375 | dependencies: 376 | "@jsdotlua/emittery": "npm:^3.6.1-rc.2" 377 | "@jsdotlua/expect": "npm:^3.6.1-rc.2" 378 | "@jsdotlua/jest-fake-timers": "npm:^3.6.1-rc.2" 379 | "@jsdotlua/jest-mock": "npm:^3.6.1-rc.2" 380 | "@jsdotlua/jest-snapshot": "npm:^3.6.1-rc.2" 381 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 382 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 383 | "@jsdotlua/promise": "npm:^3.5.0" 384 | checksum: 280a8142a0aec3b86446b456764f536f9bf71523f564d17e4f65a6ea13d3da1fa820eeac181101370d725e3233aa22968ad4311a2c21df7db776655dbdffa444 385 | languageName: node 386 | linkType: hard 387 | 388 | "@jsdotlua/jest-snapshot@npm:^3.6.1-rc.2": 389 | version: 3.6.1-rc.2 390 | resolution: "@jsdotlua/jest-snapshot@npm:3.6.1-rc.2" 391 | dependencies: 392 | "@jsdotlua/chalk": "npm:^0.2.1" 393 | "@jsdotlua/jest-diff": "npm:^3.6.1-rc.2" 394 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 395 | "@jsdotlua/jest-matcher-utils": "npm:^3.6.1-rc.2" 396 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 397 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 398 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 399 | "@jsdotlua/pretty-format": "npm:^3.6.1-rc.2" 400 | "@jsdotlua/promise": "npm:^3.5.0" 401 | checksum: e6fef6faf7a2696aa9f032805eebb8b82c72742bdb423c9c91bea8c6ddd7ea625554d990c2ade294a108f87bea8cd7e9ad4d59f3283a97dd72087ec99833960c 402 | languageName: node 403 | linkType: hard 404 | 405 | "@jsdotlua/jest-test-result@npm:^3.6.1-rc.2": 406 | version: 3.6.1-rc.2 407 | resolution: "@jsdotlua/jest-test-result@npm:3.6.1-rc.2" 408 | dependencies: 409 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 410 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 411 | checksum: f43d418941805671415c4f68581f068803079f73d4cc4757fb72f717783540c04ac02d0856d7faa7c28873d17898cf84417b695c7ca4b939bf5aa75e0353ef3f 412 | languageName: node 413 | linkType: hard 414 | 415 | "@jsdotlua/jest-types@npm:^3.6.1-rc.2": 416 | version: 3.6.1-rc.2 417 | resolution: "@jsdotlua/jest-types@npm:3.6.1-rc.2" 418 | dependencies: 419 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 420 | luau-regexp: "npm:^0.2.1" 421 | checksum: 8777716c40e76d7bab203db368af2b67cfba7fccef9acc36c91150b4c25628da06e85f99b5bf207449cb3d51ea5bd9cc9fb8ede5102afa9ea3f9854a7a66570e 422 | languageName: node 423 | linkType: hard 424 | 425 | "@jsdotlua/jest-util@npm:^3.6.1-rc.2": 426 | version: 3.6.1-rc.2 427 | resolution: "@jsdotlua/jest-util@npm:3.6.1-rc.2" 428 | dependencies: 429 | "@jsdotlua/chalk": "npm:^0.2.1" 430 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 431 | "@jsdotlua/jest-types": "npm:^3.6.1-rc.2" 432 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 433 | "@jsdotlua/picomatch": "npm:^0.4.0" 434 | "@jsdotlua/promise": "npm:^3.5.0" 435 | luau-regexp: "npm:^0.2.1" 436 | checksum: b9a5d7ba8fe9f104922f84b493efe28a0f4622658c625a4b97d7fceb8e07651dd9b0fd20c9195c2c13c47e9569e9c6e60e081ed5cb17165d2fb51f28a7ac8ea3 437 | languageName: node 438 | linkType: hard 439 | 440 | "@jsdotlua/jest-validate@npm:^3.6.1-rc.2": 441 | version: 3.6.1-rc.2 442 | resolution: "@jsdotlua/jest-validate@npm:3.6.1-rc.2" 443 | dependencies: 444 | "@jsdotlua/chalk": "npm:^0.2.1" 445 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 446 | checksum: 50e51e5d3ae790bd51132d82797d0931ab8866b945fa64b5890649074bd4fc06c6e4f6afe207ab0c4817a90d8fd2159504e5c9acb2328396de7715cea4e5a854 447 | languageName: node 448 | linkType: hard 449 | 450 | "@jsdotlua/jest@npm:^3.6.1-rc.2": 451 | version: 3.6.1-rc.2 452 | resolution: "@jsdotlua/jest@npm:3.6.1-rc.2" 453 | dependencies: 454 | "@jsdotlua/jest-core": "npm:^3.6.1-rc.2" 455 | checksum: 3359aca93518fa2b66c6f98034db4a6026b278a973256949ccd9cf3f8ae7f614976de79b7863d7387188bf3322036d2b4497ed35fc74801ece5099352fa079bb 456 | languageName: node 457 | linkType: hard 458 | 459 | "@jsdotlua/luau-polyfill@npm:^1.2.6": 460 | version: 1.2.7 461 | resolution: "@jsdotlua/luau-polyfill@npm:1.2.7" 462 | dependencies: 463 | "@jsdotlua/boolean": "npm:^1.2.7" 464 | "@jsdotlua/collections": "npm:^1.2.7" 465 | "@jsdotlua/console": "npm:^1.2.7" 466 | "@jsdotlua/es7-types": "npm:^1.2.7" 467 | "@jsdotlua/instance-of": "npm:^1.2.7" 468 | "@jsdotlua/math": "npm:^1.2.7" 469 | "@jsdotlua/number": "npm:^1.2.7" 470 | "@jsdotlua/string": "npm:^1.2.7" 471 | "@jsdotlua/timers": "npm:^1.2.7" 472 | symbol-luau: "npm:^1.0.0" 473 | checksum: 1f98940c9f7cff669c9202370e2faa1e577ec2c7fd32c85773fcf20a00a6255dc99dd49f8962799db30526f436b08af4d9710c86b2d2d30b9184da4b60620c5c 474 | languageName: node 475 | linkType: hard 476 | 477 | "@jsdotlua/math@npm:^1.2.7": 478 | version: 1.2.7 479 | resolution: "@jsdotlua/math@npm:1.2.7" 480 | checksum: 4e622511789a89d6c92c7c03856ac18763b402c1638b482df29dee43e56edd662b1131f27945919eb9a3960c26196767ca9e12bea118aa6c9781d2b10807291f 481 | languageName: node 482 | linkType: hard 483 | 484 | "@jsdotlua/number@npm:^1.2.7": 485 | version: 1.2.7 486 | resolution: "@jsdotlua/number@npm:1.2.7" 487 | checksum: ffb9ee335508f90f381419616e066bed3b294d3d77bbda73378fcabf3ccd1881b9f69364d1f4ed979e8b870c478a860a79921e12f7efbeb4b310e4208b75edfd 488 | languageName: node 489 | linkType: hard 490 | 491 | "@jsdotlua/path@npm:^3.6.1-rc.2": 492 | version: 3.6.1-rc.2 493 | resolution: "@jsdotlua/path@npm:3.6.1-rc.2" 494 | dependencies: 495 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 496 | checksum: 60868d5161f3bfc263ebcddac8cbe83f6787f85409fc1a563ff350b9ccab05c88e934ccd7e3627ea77bd73f5e30b21dbe7d85e7a4d8445d3baf96cd42bab4877 497 | languageName: node 498 | linkType: hard 499 | 500 | "@jsdotlua/picomatch@npm:^0.4.0": 501 | version: 0.4.0 502 | resolution: "@jsdotlua/picomatch@npm:0.4.0" 503 | dependencies: 504 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 505 | "@jsdotlua/promise": "npm:^3.5.0" 506 | luau-regexp: "npm:^0.2.1" 507 | checksum: 7ccc76cfe19974a91758adf62fc69529d05815575488c1ba31af052b0f25de24a9614fd47c6e5bf67f6c28aa8e84f3f32bb59f6cb59143bb05d41b925b3a2b0e 508 | languageName: node 509 | linkType: hard 510 | 511 | "@jsdotlua/pretty-format@npm:^3.6.1-rc.2": 512 | version: 3.6.1-rc.2 513 | resolution: "@jsdotlua/pretty-format@npm:3.6.1-rc.2" 514 | dependencies: 515 | "@jsdotlua/chalk": "npm:^0.2.1" 516 | "@jsdotlua/jest-get-type": "npm:^3.6.1-rc.2" 517 | "@jsdotlua/jest-roblox-shared": "npm:^3.6.1-rc.2" 518 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 519 | "@jsdotlua/react-is": "npm:^17.1.0" 520 | luau-regexp: "npm:^0.2.1" 521 | checksum: c82724cf7d9f84e4a12dd24fa8a13238649a76182537cc02e973a88720bf007a538d6ffea6fdb771cae6421ceea7aa221d58f126e1ab0b54f617294e7c455dfd 522 | languageName: node 523 | linkType: hard 524 | 525 | "@jsdotlua/promise@npm:^3.5.0": 526 | version: 3.5.0 527 | resolution: "@jsdotlua/promise@npm:3.5.0" 528 | checksum: 8771dac525b8d608ad319040b742cdde8f4cbbd4a075ae02dad4d510c8ab739dabf7de40ae1b15211e3890331cf3e7d9454fdcb3ec9a08471663436c0e9ec977 529 | languageName: node 530 | linkType: hard 531 | 532 | "@jsdotlua/react-is@npm:^17.1.0": 533 | version: 17.2.0 534 | resolution: "@jsdotlua/react-is@npm:17.2.0" 535 | dependencies: 536 | "@jsdotlua/shared": "npm:^17.2.0" 537 | checksum: b0d1dd43d789e8db524f5f5cbbe10621cd7adf2740e899c99a490570263b34eaa7a170875ae406319bd592a64a41b4b91feccef432b62461ee8e5dac4ece69f8 538 | languageName: node 539 | linkType: hard 540 | 541 | "@jsdotlua/react-reconciler@npm:^17.2.0": 542 | version: 17.2.0 543 | resolution: "@jsdotlua/react-reconciler@npm:17.2.0" 544 | dependencies: 545 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 546 | "@jsdotlua/promise": "npm:^3.5.0" 547 | "@jsdotlua/react": "npm:^17.2.0" 548 | "@jsdotlua/scheduler": "npm:^17.2.0" 549 | "@jsdotlua/shared": "npm:^17.2.0" 550 | checksum: 462c8f31c8685591eb184126313696917f3ca6c776c1fcaac896156b6be38555a28d664878963bd4aec28f2580f7831e9d52183d7617f044d6c5a2d1328f753b 551 | languageName: node 552 | linkType: hard 553 | 554 | "@jsdotlua/react-roblox@npm:^17.1.0": 555 | version: 17.2.0 556 | resolution: "@jsdotlua/react-roblox@npm:17.2.0" 557 | dependencies: 558 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 559 | "@jsdotlua/react": "npm:^17.2.0" 560 | "@jsdotlua/react-reconciler": "npm:^17.2.0" 561 | "@jsdotlua/scheduler": "npm:^17.2.0" 562 | "@jsdotlua/shared": "npm:^17.2.0" 563 | checksum: 8b8b9a847629af16e60386321c169ae5d90a8cf0654099e07841d0e26af3e7873fc4d62d5384740a32b0d0e3313b2e8c6b7d96ff804fcb5e457fa37f7af5df43 564 | languageName: node 565 | linkType: hard 566 | 567 | "@jsdotlua/react-testing-library@npm:^12.2.1-rc.1": 568 | version: 12.2.1-rc.1 569 | resolution: "@jsdotlua/react-testing-library@npm:12.2.1-rc.1" 570 | dependencies: 571 | "@jsdotlua/dom-testing-library": "npm:^8.14.2-rc.1" 572 | "@jsdotlua/jest-globals": "npm:^3.6.1-rc.2" 573 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 574 | "@jsdotlua/promise": "npm:^3.5.0" 575 | "@jsdotlua/react": "npm:^17.1.0" 576 | "@jsdotlua/react-roblox": "npm:^17.1.0" 577 | "@jsdotlua/scheduler": "npm:^17.1.0" 578 | "@jsdotlua/shared": "npm:^17.1.0" 579 | checksum: 2c7f4cf8727d17769f300b30c118e70545941d93aa4c3f51bb7f8eaa455422badafc135bd9fd350477781eaf64bd46e28854844397e447b6fc8597b9199602f7 580 | languageName: node 581 | linkType: hard 582 | 583 | "@jsdotlua/react@npm:^17.1.0, @jsdotlua/react@npm:^17.2.0": 584 | version: 17.2.0 585 | resolution: "@jsdotlua/react@npm:17.2.0" 586 | dependencies: 587 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 588 | "@jsdotlua/shared": "npm:^17.2.0" 589 | checksum: 3a91ac2fe799b94c468333404b2e5f8984c9857f03cbd3be953d0211cc50729575d451ea13c428c7f26814597125f8ec1c5f74ad2c29f3bf354751d4ddb8c143 590 | languageName: node 591 | linkType: hard 592 | 593 | "@jsdotlua/scheduler@npm:^17.1.0, @jsdotlua/scheduler@npm:^17.2.0": 594 | version: 17.2.0 595 | resolution: "@jsdotlua/scheduler@npm:17.2.0" 596 | dependencies: 597 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 598 | "@jsdotlua/shared": "npm:^17.2.0" 599 | checksum: c693dc5acdac7c6f6d52cc6df204a90d32d98f131c1d43d7a33e6c62e809863529bb0d3e6608d43ea215385a25be1cd198c0a5e8b4de1fff0c8f318ec5c145e8 600 | languageName: node 601 | linkType: hard 602 | 603 | "@jsdotlua/shared@npm:^17.1.0, @jsdotlua/shared@npm:^17.2.0": 604 | version: 17.2.0 605 | resolution: "@jsdotlua/shared@npm:17.2.0" 606 | dependencies: 607 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 608 | checksum: 01cc74fdab68e34b5ba98bd5d5d09093606cc4f5ce59b0fe61da0dccb350194b8666bad9633ddba8c8ba18e3ff0846225d183ed4da7dc85388b97e2c0a78312b 609 | languageName: node 610 | linkType: hard 611 | 612 | "@jsdotlua/string@npm:^1.2.7": 613 | version: 1.2.7 614 | resolution: "@jsdotlua/string@npm:1.2.7" 615 | dependencies: 616 | "@jsdotlua/es7-types": "npm:^1.2.7" 617 | "@jsdotlua/number": "npm:^1.2.7" 618 | checksum: 2a4fea39734748f5b0e188b143cd178f460e32998745faf3b99364cb51fdd354d77c9c2a2138f28a71c5dad2cf10112a4368017aa2c9b12c36d198b84ccb079c 619 | languageName: node 620 | linkType: hard 621 | 622 | "@jsdotlua/throat@npm:^3.6.1-rc.2": 623 | version: 3.6.1-rc.2 624 | resolution: "@jsdotlua/throat@npm:3.6.1-rc.2" 625 | dependencies: 626 | "@jsdotlua/luau-polyfill": "npm:^1.2.6" 627 | "@jsdotlua/promise": "npm:^3.5.0" 628 | checksum: 306215bdc4d7baf8ff79cece3b6038123ea7b9d4fa41eaaba4ec9f0c9c48a8ed2e82bdbc1a256b16257188a8236d84deac4131a9a15e0a40c48aa51e93e285a6 629 | languageName: node 630 | linkType: hard 631 | 632 | "@jsdotlua/timers@npm:^1.2.7": 633 | version: 1.2.7 634 | resolution: "@jsdotlua/timers@npm:1.2.7" 635 | dependencies: 636 | "@jsdotlua/collections": "npm:^1.2.7" 637 | checksum: ce7c1aeb0278436319325f45c6866f3a580453ca1e094ca9641421083268283b8e896a90e249108d89b8700fdd0571d39e687e94e2f27561628d5c83b33db673 638 | languageName: node 639 | linkType: hard 640 | 641 | "@seaofvoices/react-lua-hooks@workspace:packages/react-lua-hooks": 642 | version: 0.0.0-use.local 643 | resolution: "@seaofvoices/react-lua-hooks@workspace:packages/react-lua-hooks" 644 | dependencies: 645 | "@jsdotlua/jest-globals": "npm:^3.6.1-rc.2" 646 | "@jsdotlua/react": "npm:^17.1.0" 647 | "@jsdotlua/react-testing-library": "npm:^12.2.1-rc.1" 648 | "@seaofvoices/react-lua-use-constant": "workspace:^" 649 | "@seaofvoices/react-render-hook": "workspace:^" 650 | luau-teardown: "npm:^0.1.4" 651 | npmluau: "npm:^0.1.1" 652 | languageName: unknown 653 | linkType: soft 654 | 655 | "@seaofvoices/react-lua-use-constant@workspace:^, @seaofvoices/react-lua-use-constant@workspace:packages/react-lua-use-constant": 656 | version: 0.0.0-use.local 657 | resolution: "@seaofvoices/react-lua-use-constant@workspace:packages/react-lua-use-constant" 658 | dependencies: 659 | "@jsdotlua/react": "npm:^17.1.0" 660 | npmluau: "npm:^0.1.1" 661 | languageName: unknown 662 | linkType: soft 663 | 664 | "@seaofvoices/react-render-hook@workspace:^, @seaofvoices/react-render-hook@workspace:packages/react-render-hook": 665 | version: 0.0.0-use.local 666 | resolution: "@seaofvoices/react-render-hook@workspace:packages/react-render-hook" 667 | dependencies: 668 | "@jsdotlua/react": "npm:^17.1.0" 669 | "@jsdotlua/react-testing-library": "npm:^12.2.1-rc.1" 670 | npmluau: "npm:^0.1.1" 671 | languageName: unknown 672 | linkType: soft 673 | 674 | "@seaofvoices/react-roblox-hooks@workspace:packages/react-roblox-hooks": 675 | version: 0.0.0-use.local 676 | resolution: "@seaofvoices/react-roblox-hooks@workspace:packages/react-roblox-hooks" 677 | dependencies: 678 | "@jsdotlua/react": "npm:^17.1.0" 679 | "@seaofvoices/react-lua-use-constant": "workspace:^" 680 | "@seaofvoices/react-roblox-use-service": "workspace:^" 681 | luau-disk: "npm:^0.1.1" 682 | npmluau: "npm:^0.1.1" 683 | languageName: unknown 684 | linkType: soft 685 | 686 | "@seaofvoices/react-roblox-use-service@workspace:^, @seaofvoices/react-roblox-use-service@workspace:packages/react-roblox-use-service": 687 | version: 0.0.0-use.local 688 | resolution: "@seaofvoices/react-roblox-use-service@workspace:packages/react-roblox-use-service" 689 | dependencies: 690 | "@jsdotlua/jest-globals": "npm:^3.6.1-rc.2" 691 | "@jsdotlua/react": "npm:^17.1.0" 692 | "@seaofvoices/react-render-hook": "workspace:^" 693 | npmluau: "npm:^0.1.1" 694 | languageName: unknown 695 | linkType: soft 696 | 697 | "commander@npm:^11.0.0": 698 | version: 11.1.0 699 | resolution: "commander@npm:11.1.0" 700 | checksum: 13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179 701 | languageName: node 702 | linkType: hard 703 | 704 | "luau-disk@npm:^0.1.1": 705 | version: 0.1.1 706 | resolution: "luau-disk@npm:0.1.1" 707 | checksum: ee97edfe07671c3989f31b21cdef1001d845dbbc4940b41a2cb9b74e1c1bb0008c339348c595669177884ad8f0df4b6bf14ec66457854edcccb8e18fc3e9e4eb 708 | languageName: node 709 | linkType: hard 710 | 711 | "luau-regexp@npm:^0.2.1": 712 | version: 0.2.1 713 | resolution: "luau-regexp@npm:0.2.1" 714 | checksum: 7badc4e6f9ab7753d77e49db3012c525fb85dff70930c806754edd86893a39c1afb1aba51aba00b2f456f72d7f02c6a8812a160646a05986bc6d1bfae6251494 715 | languageName: node 716 | linkType: hard 717 | 718 | "luau-teardown@npm:^0.1.4": 719 | version: 0.1.4 720 | resolution: "luau-teardown@npm:0.1.4" 721 | checksum: 4e08d13ec8b0d20bea97fe92c9ce3163844751d73d1b6329679fbe24da5a912c7ea9061db5e935392c1d4d8309ebeae870f727651e843ec903a68edc1129ee0d 722 | languageName: node 723 | linkType: hard 724 | 725 | "npmluau@npm:^0.1.1": 726 | version: 0.1.1 727 | resolution: "npmluau@npm:0.1.1" 728 | dependencies: 729 | commander: "npm:^11.0.0" 730 | walkdir: "npm:^0.4.1" 731 | bin: 732 | npmluau: main.js 733 | checksum: 9ae22c0dcff9e85c90b4da4e8c17bc51e9b567b4a417c9767d355ff68faca4f99a2934b581743ebc8729f6851d1ba5b64597312151747252e040517d1794fbca 734 | languageName: node 735 | linkType: hard 736 | 737 | "symbol-luau@npm:^1.0.0": 738 | version: 1.0.1 739 | resolution: "symbol-luau@npm:1.0.1" 740 | checksum: ab51a77331b2d5e4666528bada17e67b26aea355257bba9e97351016cd1836bd19f372355a14cf5bef2f4d5bc6b32fe91aeb09698d7bdc079d2c61330bedf251 741 | languageName: node 742 | linkType: hard 743 | 744 | "walkdir@npm:^0.4.1": 745 | version: 0.4.1 746 | resolution: "walkdir@npm:0.4.1" 747 | checksum: 88e635aa9303e9196e4dc15013d2bd4afca4c8c8b4bb27722ca042bad213bb882d3b9141b3b0cca6bfb274f7889b30cf58d6374844094abec0016f335c5414dc 748 | languageName: node 749 | linkType: hard 750 | 751 | "workspace@workspace:.": 752 | version: 0.0.0-use.local 753 | resolution: "workspace@workspace:." 754 | dependencies: 755 | "@jsdotlua/jest": "npm:^3.6.1-rc.2" 756 | "@jsdotlua/jest-globals": "npm:^3.6.1-rc.2" 757 | npmluau: "npm:^0.1.1" 758 | languageName: unknown 759 | linkType: soft 760 | --------------------------------------------------------------------------------