├── .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 | [](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml)
4 | [](https://github.com/luau-lang/luau)
5 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 | [](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 | [](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml)
4 | 
5 | [](https://github.com/luau-lang/luau)
6 | 
7 | 
8 |
9 | [](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 | [](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml)
4 | 
5 | [](https://github.com/luau-lang/luau)
6 | 
7 | 
8 |
9 | [](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 | [](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml)
4 | 
5 | [](https://github.com/luau-lang/luau)
6 | 
7 | 
8 |
9 | [](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 | [](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml)
4 | 
5 | [](https://github.com/luau-lang/luau)
6 | 
7 | 
8 |
9 | [](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 | [](https://github.com/seaofvoices/react-lua-hooks/actions/workflows/test.yml)
4 | 
5 | [](https://github.com/luau-lang/luau)
6 | 
7 | 
8 |
9 | [](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 |
--------------------------------------------------------------------------------