diff --git a/.darklua.json b/.darklua.json new file mode 100644 index 0000000..590b458 --- /dev/null +++ b/.darklua.json @@ -0,0 +1,24 @@ +{ + "process": [ + { + "rule": "convert_require", + "current": { + "name": "path", + "sources": { + "@pkg": "node_modules/.luau-aliases" + } + }, + "target": { + "name": "roblox", + "rojo_sourcemap": "./sourcemap.json", + "indexing_style": "wait_for_child" + } + }, + "compute_expression", + "remove_unused_if_branch", + "remove_unused_while", + "filter_after_early_return", + "remove_nil_declaration", + "remove_empty_do" + ] +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d2dcf38 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +* text eol=lf + +*.gif binary +*.ico binary +*.jpg binary +*.png binary diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0a81ad0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Release + +on: workflow_dispatch + +jobs: + publish-package: + name: Publish package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Enable corepack + run: corepack enable + + - uses: actions/setup-node@v3 + with: + node-version: latest + cache: yarn + cache-dependency-path: yarn.lock + + - name: Install packages + run: yarn install --immutable + + - name: Run npmluau + run: yarn run prepare + + - name: Authenticate yarn + run: |- + yarn config set npmAlwaysAuth true + yarn config set npmAuthToken $NPM_AUTH_TOKEN + env: + NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish to npm + run: yarn workspaces foreach --all --no-private npm publish --access public --tolerate-republish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..cb8a976 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: Roblox/setup-foreman@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - uses: actions/setup-node@v3 + with: + node-version: "latest" + cache: "yarn" + cache-dependency-path: "yarn.lock" + + - name: Install packages + run: yarn install --immutable + + - name: Run npmluau + run: yarn run prepare + + - name: Run linter + run: yarn run lint:selene + + - name: Verify code style + run: yarn run style-check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b8510e --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +/site +/*.rbxl +/*.rbxlx +/*.rbxl.lock +/*.rbxlx.lock +/*.rbxm +/*.rbxmx + +/jest.lua +/roblox +/node_modules + +/.yarn + +build/ +test-places/ + +/globalTypes.d.lua + +**/sourcemap.json diff --git a/.luau-analyze.json b/.luau-analyze.json new file mode 100644 index 0000000..89f7b87 --- /dev/null +++ b/.luau-analyze.json @@ -0,0 +1,6 @@ +{ + "luau-lsp.require.mode": "relativeToFile", + "luau-lsp.require.directoryAliases": { + "@pkg": "node_modules/.luau-aliases" + } +} diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..74c3074 --- /dev/null +++ b/.luaurc @@ -0,0 +1,8 @@ +{ + "languageMode": "strict", + "lintErrors": true, + "lint": { + "*": true, + "LocalShadow": false + } +} \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..edb40bb --- /dev/null +++ b/.npmignore @@ -0,0 +1,36 @@ +/.github/ +/.vscode/ +/scripts/ +/docs/ + +/roblox +/build + +.gitattributes +CHANGELOG.md + +.yarn + +.darklua* +.luau-analyze.json +.luaurc +foreman.toml +selene.toml +selene_definitions.yml +stylua.toml +.styluaignore + +/globalTypes.d.lua +**/sourcemap.json +*.project.json + +**/__tests__ +**/*.test.lua +**/jest.config.lua + +**/*.rbxl +**/*.rbxlx +**/*.rbxl.lock +**/*.rbxlx.lock +**/*.rbxm +**/*.rbxmx diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..e25542e --- /dev/null +++ b/.styluaignore @@ -0,0 +1,4 @@ +/node_modules +/roblox + +**/*.d.lua diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f43c3e6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "luau-lsp.require.directoryAliases": { + "@pkg": "node_modules/.luau-aliases" + } +} \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..a7dab52 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Sea of Voices + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8515fcd --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# React Lua Hooks + +This project consists of two main packages that contains [react-lua](https://github.com/jsdotlua/react-lua) hooks. + +- [Lua hooks](packages/react-lua-hooks/README.md#content): general-purpose collection of hooks for [react-lua](https://github.com/jsdotlua/react-lua) +- [Roblox hooks](packages/react-roblox-hooks/README.md#content): hooks specifically made for Roblox development + +# Installation + +Add these packages to your dependencies: + +```bash +yarn add @seaofvoices/react-lua-hooks +yarn add @seaofvoices/react-roblox-hooks +``` + +Or if you are using `npm`: + +```bash +npm install @seaofvoices/react-lua-hooks +npm install @seaofvoices/react-roblox-hooks +``` + +# Content + +- [Lua hooks](packages/react-lua-hooks/README.md#content) + - [useDefaultState](packages/react-lua-hooks/README.md#usedefaultstate) + - [usePrevious](packages/react-lua-hooks/README.md#useprevious) + - [usePreviousDistinct](packages/react-lua-hooks/README.md#usepreviousdistinct) + - [useToggle](packages/react-lua-hooks/README.md#usetoggle) + - [useUnmount](packages/react-lua-hooks/README.md#useunmount) + - [useDebouncedState](packages/react-lua-hooks/README.md#usedebouncedstate) + - [useThrottledState](packages/react-lua-hooks/README.md#usethrottledstate) +- [Roblox hooks](packages/react-roblox-hooks/README.md#content) + - [useService](packages/react-roblox-hooks/README.md#useservice) + - [useCamera](packages/react-roblox-hooks/README.md#usecamera) + - [useCameraCFrame](packages/react-roblox-hooks/README.md#usecameracframe) + - [useEvent](packages/react-roblox-hooks/README.md#useevent) + - [useGuiObjectSizeBinding](packages/react-roblox-hooks/README.md#useguiobjectsizebinding) + - [useLocalPlayer](packages/react-roblox-hooks/README.md#uselocalplayer) + - [useObjectLocation](packages/react-roblox-hooks/README.md#useobjectlocation) + - [usePropertyChange](packages/react-roblox-hooks/README.md#usepropertychange) + - [useTaggedInstances](packages/react-roblox-hooks/README.md#usetaggedinstances) + - [useTextSize](packages/react-roblox-hooks/README.md#usetextsize) + - [useViewportSize](packages/react-roblox-hooks/README.md#useviewportsize) + +# Other Lua Environments Support + +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. + +The library uses [darklua](https://github.com/seaofvoices/darklua) to process its code. diff --git a/foreman.toml b/foreman.toml new file mode 100644 index 0000000..c17064b --- /dev/null +++ b/foreman.toml @@ -0,0 +1,7 @@ +[tools] +rojo = { github = "rojo-rbx/rojo", version = "=7.3.0" } +selene = { github = "Kampfkarren/selene", version = "=0.25.0" } +stylua = { github = "JohnnyMorganz/styLua", version = "=0.18.2" } +darklua = { github = "seaofvoices/darklua", version = "=0.11.3" } +luau-lsp = { github = "JohnnyMorganz/luau-lsp", version = "=1.27.0" } +run-in-roblox = { github = "rojo-rbx/run-in-roblox", version = "=0.3.0" } diff --git a/package.json b/package.json new file mode 100644 index 0000000..9697396 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "workspace", + "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "prepare": "npmluau", + "lint": "sh ./scripts/analyze.sh && selene packages", + "lint:luau": "sh ./scripts/analyze.sh", + "lint:selene": "selene packages", + "format": "stylua .", + "style-check": "stylua . --check", + "verify-pack": "yarn workspaces foreach -A --no-private pack --dry-run", + "clean": "rm -rf node_modules" + }, + "devDependencies": { + "npmluau": "^0.1.1" + }, + "packageManager": "yarn@4.0.2" +} diff --git a/packages/react-lua-hooks/CHANGELOG.md b/packages/react-lua-hooks/CHANGELOG.md new file mode 100644 index 0000000..958b858 --- /dev/null +++ b/packages/react-lua-hooks/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +* Initial version diff --git a/packages/react-lua-hooks/README.md b/packages/react-lua-hooks/README.md new file mode 100644 index 0000000..1072de0 --- /dev/null +++ b/packages/react-lua-hooks/README.md @@ -0,0 +1,172 @@ +# react-lua-hooks + +This package is a collection of hooks for [react-lua](https://github.com/jsdotlua/react-lua). + +## Installation + +Add `react-lua-hooks` in your dependencies: + +```bash +yarn add @seaofvoices/react-lua-hooks +``` + +Or if you are using `npm`: + +```bash +npm install @seaofvoices/react-lua-hooks +``` + +## Content + +**Hooks:** + +- [useDefaultState](#usedefaultstate) +- [usePrevious](#useprevious) +- [usePreviousDistinct](#usepreviousdistinct) +- [useToggle](#usetoggle) +- [useUnmount](#useunmount) +- [useDebouncedState](#usedebouncedstate) +- [useThrottledState](#usethrottledstate) + +### `useDefaultState` + +A hook that wraps `useState` to provides a default value whenever the actual state value is `nil`. + +```lua +function useDefaultState(initialValue: T?, defaultValue: T): (T, (T) -> ()) +``` + +#### Example + +```lua +local function Component(props) + local value, setValue = useDefaultState(nil, 10) +end +``` + +### `usePrevious` + +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. + +```lua +function usePrevious(value: T): T? +``` + +#### Example + +```lua +local function Component(props) + local currentValue = ... + local previousValue = usePrevious(currentValue) +end +``` + +### `usePreviousDistinct` + +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. + +Value comparison is done using `==`, but a function can be passed to customize the equality check. + +```lua +function usePreviousDistinct(value: T): T? +function usePreviousDistinct(value: T, isEqual: ((T, T) -> boolean)): T? +``` + +#### Example + +```lua +local function Component(props) + local value = useSomeState() + local previousDistinctValue = usePreviousDistinct(value) +end +``` + +### `useToggle` + +A hook to manage a boolean value. + +```lua +function useToggle(defaultValue: boolean?): (boolean, Toggle) +-- where +type Toggle = { + toggle: () -> (), + on: () -> (), + off: () -> (), +} +``` + +#### Example + +```lua +local function Component(props) + local value, toggle = useToggle(false) + + -- Somewhere in your component + toggle.toggle() -- toggles the value + toggle.on() -- set the value to true + toggle.off() -- set the value to false +end +``` + +### `useUnmount` + +A hook that executes a callback when a component is unmounted. + +```lua +function useUnmount(fn: () -> ()) +``` + +#### Example + +```lua +local function Component(props) + useUnmount(function() + -- cleanup logic when the component is unmounted + end) +end +``` + +### `useDebouncedState` + +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. + +```lua +function useDebouncedState(initialValue: T, intervalSeconds: number): (T, (T) -> ()) +``` + +#### Example + +```lua +local function Component(props) + -- debounce for 0.5 seconds + local value, setValue = useDebouncedState(1, 0.5) +end +``` + +### `useThrottledState` + +Similar to `useDebouncedState`, this hook throttles state updates, but instead of delaying updates, it limits the rate at which updates occur. + +```lua +function useThrottledState(initialValue: T, intervalSeconds: number): (T, (T) -> ()) +``` + +#### Example + +```lua +local function Component(props) + -- throttle updates to once every second + local value, setValue = useThrottledState(someValue, 1) + -- `setValue` +end +``` + +## Other Lua Environments Support + +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. + +The library uses [darklua](https://github.com/seaofvoices/darklua) to process its code. + +## License + +This project is available under the MIT license. See [LICENSE.txt](../../LICENSE.txt) for details. diff --git a/packages/react-lua-hooks/package.json b/packages/react-lua-hooks/package.json new file mode 100644 index 0000000..08b5cd5 --- /dev/null +++ b/packages/react-lua-hooks/package.json @@ -0,0 +1,26 @@ +{ + "name": "@seaofvoices/react-lua-hooks", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "https://github.com/seaofvoices/react-lua-hooks.git", + "directory": "packages/react-lua-hooks" + }, + "keywords": [ + "lua", + "luau", + "react", + "roblox", + "hook" + ], + "license": "MIT", + "main": "./src/init.lua", + "dependencies": { + "@jsdotlua/react": "^17.1.0", + "@seaofvoices/react-lua-use-constant": "workspace:^", + "luau-disk": "^0.1.0" + }, + "devDependencies": { + "npmluau": "^0.1.1" + } +} diff --git a/packages/react-lua-hooks/src/SetStateType.lua b/packages/react-lua-hooks/src/SetStateType.lua new file mode 100644 index 0000000..ced208b --- /dev/null +++ b/packages/react-lua-hooks/src/SetStateType.lua @@ -0,0 +1,6 @@ +type SetStateValue = (T) -> () +type UpdateStateValue = ((T) -> T) -> () + +export type SetState = SetStateValue & UpdateStateValue + +return nil diff --git a/packages/react-lua-hooks/src/init.lua b/packages/react-lua-hooks/src/init.lua new file mode 100644 index 0000000..ee5fff2 --- /dev/null +++ b/packages/react-lua-hooks/src/init.lua @@ -0,0 +1,22 @@ +local useConstant = require('@pkg/@seaofvoices/react-lua-use-constant') + +local useDebouncedState = require('./useDebouncedState') +local useDefaultState = require('./useDefaultState') +local usePrevious = require('./usePrevious') +local usePreviousDistinct = require('./usePreviousDistinct') +local useThrottledState = require('./useThrottledState') +local useToggle = require('./useToggle') +local useUnmount = require('./useUnmount') + +export type Toggle = useToggle.Toggle + +return { + useConstant = useConstant, + useDebouncedState = useDebouncedState, + useDefaultState = useDefaultState, + usePrevious = usePrevious, + usePreviousDistinct = usePreviousDistinct, + useThrottledState = useThrottledState, + useToggle = useToggle, + useUnmount = useUnmount, +} diff --git a/packages/react-lua-hooks/src/useDebouncedState.lua b/packages/react-lua-hooks/src/useDebouncedState.lua new file mode 100644 index 0000000..d17b313 --- /dev/null +++ b/packages/react-lua-hooks/src/useDebouncedState.lua @@ -0,0 +1,69 @@ +local React = require('@pkg/@jsdotlua/react') + +local SetStateType = require('./SetStateType') +local useUnmount = require('./useUnmount') + +local useCallback = React.useCallback +local useEffect = React.useEffect +local useRef = React.useRef +local useState = React.useState + +type SetState = SetStateType.SetState + +local function useDebouncedState(initialValue: T, intervalSeconds: number): (T, SetState) + local value, setValue = useState(initialValue) + + local scheduledValue = useRef(nil :: { value: T | (T) -> T }?) + local lastCallTime = useRef(nil :: number?) + local scheduledUpdate = useRef(nil :: thread?) + + local setDebouncedValue = useCallback(function(newValue: T | (T) -> T) + local update = scheduledUpdate.current + if update then + task.cancel(update) + end + + lastCallTime.current = os.clock() + scheduledValue.current = { value = newValue } + scheduledUpdate.current = task.delay(intervalSeconds, function() + lastCallTime.current = nil + scheduledUpdate.current = nil + setValue(newValue) + end) + end, { setValue :: any, intervalSeconds }) + + useEffect(function() + local lastCall = lastCallTime.current + local update = scheduledUpdate.current + local nextValue = scheduledValue.current + + if update and lastCall and nextValue then + scheduledUpdate.current = nil + task.cancel(update) + + if os.clock() >= lastCall + intervalSeconds then + setValue(nextValue.value) + else + scheduledUpdate.current = task.delay(intervalSeconds, function() + lastCallTime.current = nil + scheduledUpdate.current = nil + + setValue(nextValue.value) + end) + end + end + end, { intervalSeconds }) + + useUnmount(function() + local update = scheduledUpdate.current + if update then + task.cancel(update) + end + scheduledValue.current = nil + lastCallTime.current = nil + end) + + return value, setDebouncedValue +end + +return useDebouncedState diff --git a/packages/react-lua-hooks/src/useDefaultState.lua b/packages/react-lua-hooks/src/useDefaultState.lua new file mode 100644 index 0000000..88a0b66 --- /dev/null +++ b/packages/react-lua-hooks/src/useDefaultState.lua @@ -0,0 +1,15 @@ +local React = require('@pkg/@jsdotlua/react') + +local SetStateType = require('./SetStateType') + +local useState = React.useState + +type SetState = SetStateType.SetState + +local function useDefaultState(initialValue: T?, defaultValue: T): (T, SetState) + local value, setValue = useState(initialValue) + + return if value == nil then defaultValue else value, setValue +end + +return useDefaultState diff --git a/packages/react-lua-hooks/src/usePrevious.lua b/packages/react-lua-hooks/src/usePrevious.lua new file mode 100644 index 0000000..cbde181 --- /dev/null +++ b/packages/react-lua-hooks/src/usePrevious.lua @@ -0,0 +1,18 @@ +local React = require('@pkg/@jsdotlua/react') + +local useRef = React.useRef +local useEffect = React.useEffect + +local function usePrevious(value: T): T? + local ref = useRef(nil :: T?) + + ref.current = value + + useEffect(function() + ref.current = value + end) + + return ref.current +end + +return usePrevious diff --git a/packages/react-lua-hooks/src/usePreviousDistinct.lua b/packages/react-lua-hooks/src/usePreviousDistinct.lua new file mode 100644 index 0000000..408195b --- /dev/null +++ b/packages/react-lua-hooks/src/usePreviousDistinct.lua @@ -0,0 +1,21 @@ +local React = require('@pkg/@jsdotlua/react') + +local useRef = React.useRef + +local function usePreviousDistinct(value: T, isEqual: ((T, T) -> boolean)?): T? + local ref = useRef(value) + local previousValue = useRef(nil :: T?) + + local current = ref.current :: T + + local currentIsEqual = if isEqual then isEqual(current, value) else current == value + + if not currentIsEqual then + previousValue.current = current + ref.current = value + end + + return previousValue.current +end + +return usePreviousDistinct diff --git a/packages/react-lua-hooks/src/useThrottledState.lua b/packages/react-lua-hooks/src/useThrottledState.lua new file mode 100644 index 0000000..9e2fb4e --- /dev/null +++ b/packages/react-lua-hooks/src/useThrottledState.lua @@ -0,0 +1,66 @@ +local React = require('@pkg/@jsdotlua/react') + +local SetStateType = require('./SetStateType') +local useUnmount = require('./useUnmount') + +local useCallback = React.useCallback +local useEffect = React.useEffect +local useRef = React.useRef +local useState = React.useState + +type SetState = SetStateType.SetState + +local function useThrottledState(initialValue: T, intervalSeconds: number): (T, SetState) + local value, setValue = useState(initialValue) + local lastThrottleTime = useRef(os.clock()) + + local scheduledValue = useRef(nil :: { value: T | (T) -> T }?) + local scheduledUpdate = useRef(nil :: thread?) + + local setThrottledValue = useCallback(function(newValue: T | (T) -> T) + local remainingTime = intervalSeconds - (os.clock() - lastThrottleTime.current :: number) + + if remainingTime <= 0 then + lastThrottleTime.current = os.clock() + setValue(newValue) + return + else + scheduledValue.current = { value = newValue } + if scheduledUpdate.current == nil then + scheduledUpdate.current = task.delay(remainingTime, function() + scheduledUpdate.current = nil + local current = scheduledValue.current + + if current then + scheduledValue.current = nil + + lastThrottleTime.current = os.clock() + setValue(current.value) + end + end) + end + end + end, { setValue :: any, intervalSeconds }) + + useEffect(function() + local update = scheduledUpdate.current + local nextValue = scheduledValue.current + if update and nextValue then + scheduledUpdate.current = nil + task.cancel(update) + setThrottledValue(nextValue.value) + end + end, { setThrottledValue }) + + useUnmount(function() + local update = scheduledUpdate.current + if update then + task.cancel(update) + end + scheduledValue.current = nil + end) + + return value, setThrottledValue +end + +return useThrottledState diff --git a/packages/react-lua-hooks/src/useToggle.lua b/packages/react-lua-hooks/src/useToggle.lua new file mode 100644 index 0000000..52af677 --- /dev/null +++ b/packages/react-lua-hooks/src/useToggle.lua @@ -0,0 +1,36 @@ +local React = require('@pkg/@jsdotlua/react') + +local useState = React.useState +local useMemo = React.useMemo + +export type Toggle = { + toggle: () -> (), + on: () -> (), + off: () -> (), +} + +local function flipBoolean(previous: boolean): boolean + return not previous +end + +local function useToggle(defaultValue: boolean?): (boolean, Toggle) + local value, setValue = useState(defaultValue == true) + + local toggle = useMemo(function() + return { + toggle = function() + setValue(flipBoolean) + end, + on = function() + setValue(true) + end, + off = function() + setValue(false) + end, + } + end, { setValue }) + + return value, toggle +end + +return useToggle diff --git a/packages/react-lua-hooks/src/useUnmount.lua b/packages/react-lua-hooks/src/useUnmount.lua new file mode 100644 index 0000000..6b37903 --- /dev/null +++ b/packages/react-lua-hooks/src/useUnmount.lua @@ -0,0 +1,16 @@ +local React = require('@pkg/@jsdotlua/react') + +local useEffect = React.useEffect +local useRef = React.useRef + +local function useUnmount(fn: () -> ()) + local onUnmount = useRef((nil :: any) :: () -> ()) + + onUnmount.current = fn + + useEffect(function() + return (onUnmount.current :: () -> ())() + end, {}) +end + +return useUnmount diff --git a/packages/react-lua-use-constant/CHANGELOG.md b/packages/react-lua-use-constant/CHANGELOG.md new file mode 100644 index 0000000..c005b44 --- /dev/null +++ b/packages/react-lua-use-constant/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 + +* Initial version diff --git a/packages/react-lua-use-constant/README.md b/packages/react-lua-use-constant/README.md new file mode 100644 index 0000000..9c37e2b --- /dev/null +++ b/packages/react-lua-use-constant/README.md @@ -0,0 +1,41 @@ +# react-lua-use-constant + +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. + +## Installation + +Add `react-lua-use-constant` in your dependencies: + +```bash +yarn add @seaofvoices/react-lua-use-constant +``` + +Or if you are using `npm`: + +```bash +npm install @seaofvoices/react-lua-use-constant +``` + +## Usage + +This hook takes an initializer function that gets called once to obtain the constant value. + +```lua +local function Component(props) + local mountTime = useConstant(function() + return os.time() + end) + + return ... +end +``` + +## Other Lua Environments Support + +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. + +The library uses [darklua](https://github.com/seaofvoices/darklua) to process its code. + +## License + +This project is available under the MIT license. See [LICENSE.txt](../../LICENSE.txt) for details. diff --git a/packages/react-lua-use-constant/package.json b/packages/react-lua-use-constant/package.json new file mode 100644 index 0000000..a16a1e7 --- /dev/null +++ b/packages/react-lua-use-constant/package.json @@ -0,0 +1,23 @@ +{ + "name": "@seaofvoices/react-lua-use-constant", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/seaofvoices/react-lua-hooks.git", + "directory": "packages/react-lua-use-constant" + }, + "keywords": [ + "lua", + "luau", + "react", + "hook" + ], + "license": "MIT", + "main": "./src/init.lua", + "dependencies": { + "@jsdotlua/react": "^17.1.0" + }, + "devDependencies": { + "npmluau": "^0.1.1" + } +} diff --git a/packages/react-lua-use-constant/src/init.lua b/packages/react-lua-use-constant/src/init.lua new file mode 100644 index 0000000..f8ffe18 --- /dev/null +++ b/packages/react-lua-use-constant/src/init.lua @@ -0,0 +1,13 @@ +local React = require('@pkg/@jsdotlua/react') + +local function useConstant(create: () -> T): T + local ref = React.useRef(nil :: any) + + if not ref.current then + ref.current = { value = create() } + end + + return ref.current.value +end + +return useConstant diff --git a/packages/react-roblox-hooks/CHANGELOG.md b/packages/react-roblox-hooks/CHANGELOG.md new file mode 100644 index 0000000..958b858 --- /dev/null +++ b/packages/react-roblox-hooks/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +* Initial version diff --git a/packages/react-roblox-hooks/README.md b/packages/react-roblox-hooks/README.md new file mode 100644 index 0000000..5e9401a --- /dev/null +++ b/packages/react-roblox-hooks/README.md @@ -0,0 +1,284 @@ +# react-roblox-hooks + +This package is a collection of hooks for [react-lua](https://github.com/jsdotlua/react-lua), specifically target for development on Roblox. + +## Installation + +Add `react-roblox-hooks` in your dependencies: + +```bash +yarn add @seaofvoices/react-roblox-hooks +``` + +Or if you are using `npm`: + +```bash +npm install @seaofvoices/react-roblox-hooks +``` + +## Content + +**Hooks:** + +- [useService](#useservice) +- [useCamera](#usecamera) +- [useCameraCFrame](#usecameracframe) +- [useEvent](#useevent) +- [useGuiObjectSizeBinding](#useguiobjectsizebinding) +- [useLocalPlayer](#uselocalplayer) +- [useObjectLocation](#useobjectlocation) +- [usePropertyChange](#usepropertychange) +- [useTaggedInstances](#usetaggedinstances) +- [useTextSize](#usetextsize) +- [useViewportSize](#useviewportsize) + +**Components:** + +- [ServiceProvider](#serviceprovider) + +### useService + +```lua +function useService(className: string): Instance +``` + +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) + +#### Example + +```lua +local function Component(props) + local camera = useCamera() -- get the Camera object + + -- ... +end +``` + +### useCamera + +```lua +function useCamera(): Camera +``` + +A hook that returns the [CurrentCamera](https://create.roblox.com/docs/reference/engine/classes/Workspace#CurrentCamera) value from the Workspace. + +#### Example + +```lua +local function Component(props) + local camera = useCamera() -- get the Camera object + + -- ... +end +``` + +### useCameraCFrame + +```lua +function useCameraCFrame(fn: (CFrame) -> (), deps: { any }) +``` + +A hook to subscribe to each camera CFrame changes. + +Changes in the dependency array will disconnect and reconnect with the updated function. + +#### Example + +```lua +local function Component(props) + local visible, setVisible = useState(false) + + useCameraCFrame(function(cframe: CFrame) + local distance = ... -- compute distance from player + + -- trigger a state update to make something visible + -- when the camera is close enough + setVisible(distance < 30) + end, {}) + + -- ... +end +``` + +### useEvent + +A hook to subscribe to Roblox events. Runs a function when the specified event is triggered. + +Changes in the dependency array will disconnect and reconnect with the updated function. + +```lua +function useEvent( + event: RBXScriptSignal, + fn: (T...) -> (), + deps: { any } +) +``` + +#### Example + +```lua +local function Component(props) + + -- ... +end +``` + +### useGuiSizeBinding + +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. + +```lua +function useGuiSizeBinding(): (React.Ref, React.Binding) +``` + +#### Example + +```lua +local function Component(props) + local ref, binding = useGuiSizeBinding() + + return React.createElement("Frame", { + -- assign the ref to the frame and the binding will + -- automatically update with the frame AbsoluteSize property + ref = ref, + }) +end +``` + +### useLocalPlayer + +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. + +```lua +function useLocalPlayer(): Player +``` + +#### Example + +```lua +local function Component(props) + local player = useLocalPlayer(): Player + -- ... +end +``` + +### useObjectLocation + +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. + +```lua +function useObjectLocation( + object: PVInstance?, + fn: (CFrame) -> (), + deps: { any } +) +``` + +#### Example + +```lua +local function Component(props) + + -- ... +end +``` + +### usePropertyChange + +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. + +```lua +function usePropertyChange( + instance: Instance?, + property: string, + fn: (T) -> (), + deps: { any } +) +``` + +#### Example + +```lua +local function Component(props) + + -- ... +end +``` + +### useTaggedInstances + +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). + +```lua +function useTaggedInstances(tagName: string): { Instance } +function useTaggedInstances(tagName: string, mapFn: (Instance) -> T?): { T } +``` + +#### Example + +```lua +local function Component(props) + + -- ... +end +``` + +### useTextSize + +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. + +```lua +function useTextSize(text: string, options: Options): Vector2 +-- where +type Options = { size: number, font: Font, width: number? } +``` + +#### Example + +```lua +local function Component(props) + + -- ... +end +``` + +### useViewportSize + +A hook to subscribe to changes in the viewport size. It enables components to respond to screen size changes, allowing for responsive adjustments. + +```lua +function useViewportSize(fn: (Vector2) -> (), deps: { any }) +``` + +#### Example + +```lua +local function Component(props) + + -- ... +end +``` + +### ServiceProvider + +A component that can override the default service provider (which simply calls `game:GetService(className)`) with a custom implementation + +#### Example + +```lua +local function MockServiceProvider(props) + local mocks = props.mocks + local function provideMocks(className: string): Instance + -- return the mocked service or default to the real one + return mocks[className] or game:GetService(className) + end + + return React.createElement(ServiceProvider, { + value = provideMocks + }) +end +``` + +## License + +This project is available under the MIT license. See [LICENSE.txt](../../LICENSE.txt) for details. diff --git a/packages/react-roblox-hooks/package.json b/packages/react-roblox-hooks/package.json new file mode 100644 index 0000000..f56a5cb --- /dev/null +++ b/packages/react-roblox-hooks/package.json @@ -0,0 +1,27 @@ +{ + "name": "@seaofvoices/react-roblox-hooks", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "https://github.com/seaofvoices/react-lua-hooks.git", + "directory": "packages/react-roblox-hooks" + }, + "keywords": [ + "lua", + "luau", + "react", + "roblox", + "hook" + ], + "license": "MIT", + "main": "./src/init.lua", + "dependencies": { + "@jsdotlua/react": "^17.1.0", + "@seaofvoices/react-lua-use-constant": "workspace:^", + "@seaofvoices/react-roblox-use-service": "workspace:^", + "luau-disk": "^0.1.0" + }, + "devDependencies": { + "npmluau": "^0.1.1" + } +} diff --git a/packages/react-roblox-hooks/src/init.lua b/packages/react-roblox-hooks/src/init.lua new file mode 100644 index 0000000..9a384c8 --- /dev/null +++ b/packages/react-roblox-hooks/src/init.lua @@ -0,0 +1,28 @@ +local ReactUseService = require('@pkg/@seaofvoices/react-roblox-use-service') + +local useCamera = require('./useCamera') +local useCameraCFrame = require('./useCameraCFrame') +local useEvent = require('./useEvent') +local useGuiSizeBinding = require('./useGuiSizeBinding') +local useLocalPlayer = require('./useLocalPlayer') +local useObjectLocation = require('./useObjectLocation') +local usePropertyChange = require('./usePropertyChange') +local useTaggedInstances = require('./useTaggedInstances') +local useTextSize = require('./useTextSize') +local useViewportSize = require('./useViewportSize') + +return { + ServiceProvider = ReactUseService.ServiceProvider, + useService = ReactUseService.useService, + + useCamera = useCamera, + useCameraCFrame = useCameraCFrame, + useEvent = useEvent, + useGuiSizeBinding = useGuiSizeBinding, + useLocalPlayer = useLocalPlayer, + useObjectLocation = useObjectLocation, + usePropertyChange = usePropertyChange, + useTaggedInstances = useTaggedInstances, + useTextSize = useTextSize, + useViewportSize = useViewportSize, +} diff --git a/packages/react-roblox-hooks/src/useCamera.lua b/packages/react-roblox-hooks/src/useCamera.lua new file mode 100644 index 0000000..d264084 --- /dev/null +++ b/packages/react-roblox-hooks/src/useCamera.lua @@ -0,0 +1,16 @@ +local React = require('@pkg/@jsdotlua/react') +local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService + +local useMemo = React.useMemo + +local function useCamera(): Camera + local workspace = useService('Workspace') + + local camera = useMemo(function() + return workspace.CurrentCamera + end, { workspace }) + + return camera +end + +return useCamera diff --git a/packages/react-roblox-hooks/src/useCameraCFrame.lua b/packages/react-roblox-hooks/src/useCameraCFrame.lua new file mode 100644 index 0000000..7e2c971 --- /dev/null +++ b/packages/react-roblox-hooks/src/useCameraCFrame.lua @@ -0,0 +1,10 @@ +local useCamera = require('./useCamera') +local usePropertyChange = require('./usePropertyChange') + +local function useCameraCFrame(fn: (CFrame) -> (), deps: { any }) + local camera = useCamera() + + usePropertyChange(camera, 'CFrame', fn, deps) +end + +return useCameraCFrame diff --git a/packages/react-roblox-hooks/src/useEvent.lua b/packages/react-roblox-hooks/src/useEvent.lua new file mode 100644 index 0000000..68e480f --- /dev/null +++ b/packages/react-roblox-hooks/src/useEvent.lua @@ -0,0 +1,17 @@ +local React = require('@pkg/@jsdotlua/react') + +local useEffect = React.useEffect +local useCallback = React.useCallback + +local function useEvent(event: RBXScriptSignal, fn: (T...) -> (), deps: { any }) + local memoizedFn = useCallback(fn, deps) + + useEffect(function() + local connection = event:Connect(memoizedFn) + return function() + connection:Disconnect() + end + end, { event :: any, memoizedFn }) +end + +return useEvent diff --git a/packages/react-roblox-hooks/src/useGuiSizeBinding.lua b/packages/react-roblox-hooks/src/useGuiSizeBinding.lua new file mode 100644 index 0000000..3d37c9e --- /dev/null +++ b/packages/react-roblox-hooks/src/useGuiSizeBinding.lua @@ -0,0 +1,32 @@ +local React = require('@pkg/@jsdotlua/react') + +local useBinding = React.useBinding +local useRef = React.useRef +local useEffect = React.useEffect + +local function useGuiSizeBinding(): (React.Ref, React.Binding) + local ref = useRef(nil :: GuiObject?) + local binding, setBinding = useBinding(Vector2.zero) + + local current = ref.current + + useEffect(function() + if current == nil or not current:IsA('GuiBase2d') then + setBinding(Vector2.zero) + return + end + local current = current :: GuiObject + + local connection = current:GetPropertyChangedSignal('AbsoluteSize'):Connect(function() + setBinding(current.AbsoluteSize) + end) + + return function() + connection:Disconnect() + end + end, { current }) + + return ref, binding +end + +return useGuiSizeBinding diff --git a/packages/react-roblox-hooks/src/useLocalPlayer.lua b/packages/react-roblox-hooks/src/useLocalPlayer.lua new file mode 100644 index 0000000..5fb77fc --- /dev/null +++ b/packages/react-roblox-hooks/src/useLocalPlayer.lua @@ -0,0 +1,9 @@ +local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService + +local function useLocalPlayer(): Player + local player = useService('Players').LocalPlayer + + return player +end + +return useLocalPlayer diff --git a/packages/react-roblox-hooks/src/useObjectLocation.lua b/packages/react-roblox-hooks/src/useObjectLocation.lua new file mode 100644 index 0000000..501ef74 --- /dev/null +++ b/packages/react-roblox-hooks/src/useObjectLocation.lua @@ -0,0 +1,30 @@ +local React = require('@pkg/@jsdotlua/react') + +local useCallback = React.useCallback +local useEffect = React.useEffect + +local function useObjectLocation(object: PVInstance?, fn: (CFrame) -> (), deps: { any }) + local memoizedFn = useCallback(fn, deps) + + useEffect(function() + if not object or not object:IsA('PVInstance') then + return + end + local object = object :: PVInstance + + local function onChange() + local pivot = object:GetPivot() + memoizedFn(pivot) + end + + local connection = object.Changed:Connect(onChange) + + onChange() + + return function() + connection:Disconnect() + end + end, { object :: any, memoizedFn }) +end + +return useObjectLocation diff --git a/packages/react-roblox-hooks/src/usePropertyChange.lua b/packages/react-roblox-hooks/src/usePropertyChange.lua new file mode 100644 index 0000000..76699d2 --- /dev/null +++ b/packages/react-roblox-hooks/src/usePropertyChange.lua @@ -0,0 +1,32 @@ +local React = require('@pkg/@jsdotlua/react') + +local useEffect = React.useEffect +local useCallback = React.useCallback + +local function usePropertyChange( + instance: Instance?, + property: string, + fn: (T) -> (), + deps: { any } +) + local memoizedFn = useCallback(fn, deps) + + useEffect(function() + if instance == nil then + return + end + local instance = instance :: Instance + + local connection = instance:GetPropertyChangedSignal(property):Connect(function() + memoizedFn((instance :: any)[property]) + end) + + memoizedFn((instance :: any)[property]) + + return function() + connection:Disconnect() + end + end, { instance or false :: any, property, memoizedFn }) +end + +return usePropertyChange diff --git a/packages/react-roblox-hooks/src/useTaggedInstances.lua b/packages/react-roblox-hooks/src/useTaggedInstances.lua new file mode 100644 index 0000000..d6a7a78 --- /dev/null +++ b/packages/react-roblox-hooks/src/useTaggedInstances.lua @@ -0,0 +1,56 @@ +local Disk = require('@pkg/luau-disk') +local React = require('@pkg/@jsdotlua/react') +local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService + +local Array = Disk.Array + +local function useTaggedInstances(tagName: string, mapFn: ((Instance) -> T?)?): { T } + local instances, setInstances = React.useState({} :: { Instance }) + local collectionService = useService('CollectionService') + + React.useEffect(function() + local addedConnection = collectionService + :GetInstanceAddedSignal(tagName) + :Connect(function(newInstance: Instance) + setInstances(function(previous) + return Array.push(previous, newInstance) + end) + end) + + local removedConnection = collectionService + :GetInstanceRemovedSignal(tagName) + :Connect(function(removedInstance) + setInstances(function(previous) + local index = table.find(previous, removedInstance) + if index == nil then + return previous + else + local cloned = table.clone(previous) + table.remove(cloned, index) + return cloned + end + end) + end) + + setInstances(collectionService:GetTagged(tagName)) + + return function() + addedConnection:Disconnect() + removedConnection:Disconnect() + end + end, { tagName :: any, collectionService }) + + local values = React.useMemo(function() + if mapFn == nil then + return instances + else + return Array.map(instances, mapFn) + end + end, { instances :: any, mapFn or false }) + + return (values :: any) :: { T } +end + +return (useTaggedInstances :: any) :: ( + ((tagName: string, mapFn: (Instance) -> T?) -> { T }) & ((tagName: string) -> { Instance }) +) diff --git a/packages/react-roblox-hooks/src/useTextSize.lua b/packages/react-roblox-hooks/src/useTextSize.lua new file mode 100644 index 0000000..f876fc5 --- /dev/null +++ b/packages/react-roblox-hooks/src/useTextSize.lua @@ -0,0 +1,31 @@ +local React = require('@pkg/@jsdotlua/react') +local useService = require('@pkg/@seaofvoices/react-roblox-use-service').useService + +local function useTextSize( + text: string, + options: { size: number, font: Font, width: number? } +): Vector2 + local textService = useService('TextService') + + local textSize = options.size + local textFont = options.font + local width = options.width or 0 + + local textBoundsParams = React.useMemo(function() + local params = Instance.new('GetTextBoundsParams') + params.Font = textFont + params.Size = textSize + params.Width = width + return params + end, { textFont :: any, textSize, width }) + + local frameSize = React.useMemo(function() + local params = textBoundsParams:Clone() + params.Text = text + return textService:GetTextBoundsAsync(params) + end, { text :: any, textBoundsParams, textService }) + + return frameSize +end + +return useTextSize diff --git a/packages/react-roblox-hooks/src/useViewportSize.lua b/packages/react-roblox-hooks/src/useViewportSize.lua new file mode 100644 index 0000000..02772bf --- /dev/null +++ b/packages/react-roblox-hooks/src/useViewportSize.lua @@ -0,0 +1,10 @@ +local useCamera = require('./useCamera') +local usePropertyChange = require('./usePropertyChange') + +local function useViewportSize(fn: (Vector2) -> (), deps: { any }) + local camera = useCamera() + + usePropertyChange(camera, 'ViewportSize', fn, deps) +end + +return useViewportSize diff --git a/packages/react-roblox-use-service/CHANGELOG.md b/packages/react-roblox-use-service/CHANGELOG.md new file mode 100644 index 0000000..c005b44 --- /dev/null +++ b/packages/react-roblox-use-service/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 + +* Initial version diff --git a/packages/react-roblox-use-service/README.md b/packages/react-roblox-use-service/README.md new file mode 100644 index 0000000..ff3aef2 --- /dev/null +++ b/packages/react-roblox-use-service/README.md @@ -0,0 +1,51 @@ +# react-roblox-use-service + +A Luau library for Roblox that provides a `useService` hook and a `ServiceProvider` component for [react-lua](https://github.com/jsdotlua/react-lua). + +## Installation + +Add `react-roblox-use-service` in your dependencies: + +```bash +yarn add @seaofvoices/react-roblox-use-service +``` + +Or if you are using `npm`: + +```bash +npm install @seaofvoices/react-roblox-use-service +``` + +## API + +### useService + +```lua +function useService(className: string): Instance +``` + +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) + +### ServiceProvider + +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. + +#### Example + +```lua +local function MockServiceProvider(props) + local mocks = props.mocks + local function provideMocks(className: string): Instance + -- return the mocked service or default to the real one + return mocks[className] or game:GetService(className) + end + + return React.createElement(ServiceProvider, { + value = provideMocks + }) +end +``` + +## License + +This project is available under the MIT license. See [LICENSE.txt](LICENSE.txt) for details. diff --git a/packages/react-roblox-use-service/package.json b/packages/react-roblox-use-service/package.json new file mode 100644 index 0000000..e6479aa --- /dev/null +++ b/packages/react-roblox-use-service/package.json @@ -0,0 +1,24 @@ +{ + "name": "@seaofvoices/react-roblox-use-service", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/seaofvoices/react-lua-hooks.git", + "directory": "packages/react-roblox-use-service" + }, + "keywords": [ + "lua", + "luau", + "react", + "roblox", + "hook" + ], + "license": "MIT", + "main": "./src/init.lua", + "dependencies": { + "@jsdotlua/react": "^17.1.0" + }, + "devDependencies": { + "npmluau": "^0.1.1" + } +} diff --git a/packages/react-roblox-use-service/src/ServiceProviderContext.lua b/packages/react-roblox-use-service/src/ServiceProviderContext.lua new file mode 100644 index 0000000..9377e9d --- /dev/null +++ b/packages/react-roblox-use-service/src/ServiceProviderContext.lua @@ -0,0 +1,11 @@ +local React = require('@pkg/@jsdotlua/react') + +local function defaultServiceProvider(className: string): Instance + return game:GetService(className) +end + +local ServiceProviderContext: React.Context<(className: string) -> Instance> = + React.createContext(defaultServiceProvider) +ServiceProviderContext.displayName = 'ServiceProviderContext' + +return ServiceProviderContext diff --git a/packages/react-roblox-use-service/src/init.lua b/packages/react-roblox-use-service/src/init.lua new file mode 100644 index 0000000..4cca3a6 --- /dev/null +++ b/packages/react-roblox-use-service/src/init.lua @@ -0,0 +1,7 @@ +local ServiceProviderContext = require('./ServiceProviderContext') +local useService = require('./useService') + +return { + useService = useService, + ServiceProvider = ServiceProviderContext.Provider, +} diff --git a/packages/react-roblox-use-service/src/useService.lua b/packages/react-roblox-use-service/src/useService.lua new file mode 100644 index 0000000..6be3f0f --- /dev/null +++ b/packages/react-roblox-use-service/src/useService.lua @@ -0,0 +1,29 @@ +local React = require('@pkg/@jsdotlua/react') + +local ServiceProviderContext = require('./ServiceProviderContext') + +local function useService(className: string): Instance + local serviceProvider = React.useContext(ServiceProviderContext) + + return serviceProvider(className) +end + +type useServiceFn = + (('RunService') -> RunService) + & (('ReplicatedFirst') -> ReplicatedFirst) + & (('ReplicatedStorage') -> ReplicatedStorage) + & (('CollectionService') -> CollectionService) + & (('Players') -> Players) + & (('TweenService') -> TweenService) + & (('ContextActionService') -> ContextActionService) + & (('MarketplaceService') -> MarketplaceService) + & (('Lighting') -> Lighting) + & (('TextService') -> TextService) + & (('TextChatService') -> TextChatService) + & (('HttpService') -> HttpService) + & (('PhysicsService') -> PhysicsService) + & (('Workspace') -> Workspace) + & (('UserInputService') -> UserInputService) + & ((className: string) -> Instance) + +return (useService :: any) :: useServiceFn diff --git a/scripts/analyze.sh b/scripts/analyze.sh new file mode 100755 index 0000000..300e6d2 --- /dev/null +++ b/scripts/analyze.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +TYPES_FILE=globalTypes.d.lua + +if [ ! -f "$TYPES_FILE" ]; then + curl https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.lua > $TYPES_FILE +fi + +luau-lsp analyze --base-luaurc=.luaurc --settings=.luau-analyze.json \ + --definitions=$TYPES_FILE \ + packages + +selene packages diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..af9600f --- /dev/null +++ b/selene.toml @@ -0,0 +1,8 @@ +std = "selene_definitions" + +[rules] +global_usage = "allow" +shadowing = "allow" + +[config] +empty_if = { comments_count = true } diff --git a/selene_definitions.yml b/selene_definitions.yml new file mode 100644 index 0000000..95cabdf --- /dev/null +++ b/selene_definitions.yml @@ -0,0 +1,7 @@ +base: roblox +name: selene_defs +globals: + # override Roblox require style with string requires + require: + args: + - type: string diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..49b89b3 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,8 @@ +column_width = 100 +quote_style = "AutoPreferSingle" +indent_type = "Spaces" +line_endings = "Unix" +indent_width = 4 + +[sort_requires] +enabled = true diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..025b0d5 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,207 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@jsdotlua/boolean@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/boolean@npm:1.2.6" + dependencies: + "@jsdotlua/number": "npm:^1.2.6" + checksum: 5f567e556bb7c56102327a97468571522ce939be6cdc1b51591afc3dfc1dff4b2c9ec5462e7809bd9a515b3bdfc1c3eaabb4f631892cb5f471b4fc8da188cb84 + languageName: node + linkType: hard + +"@jsdotlua/collections@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/collections@npm:1.2.6" + dependencies: + "@jsdotlua/es7-types": "npm:^1.2.6" + "@jsdotlua/instance-of": "npm:^1.2.6" + checksum: a4b3fe826358484528f10e8ca61a80d841a6515a22277a003117449da66ea1fb891c7b4103b952b01bd7d75666f26cb41c8efb852672aa8aaaaebbcd656dca79 + languageName: node + linkType: hard + +"@jsdotlua/console@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/console@npm:1.2.6" + dependencies: + "@jsdotlua/collections": "npm:^1.2.6" + checksum: eff658d9e8d23f932893facf18400a29ada54d1a24748b751aac1e4df48e92ff70b3b2ccd1ab6a4f2490dd709f826fe39427fb80f01a7574265fdd4e8af9187a + languageName: node + linkType: hard + +"@jsdotlua/es7-types@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/es7-types@npm:1.2.6" + checksum: e5710f5f6de0608aec22744845e3a24acc47e2bda6cfc0b1fe4040935039b73e8467da90c3a6c9d75b08c9f39d2b98e9d72d54f3f630012538a7d53ad828a0d6 + languageName: node + linkType: hard + +"@jsdotlua/instance-of@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/instance-of@npm:1.2.6" + checksum: 0958caf214bb0556c1dcf94096410c367f413af4b02ef3adbd733e47435d8734b583217804bed5aec8308d56ccfb39032bb12fd4337e01ec5ae2db690b2ca9ce + languageName: node + linkType: hard + +"@jsdotlua/luau-polyfill@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/luau-polyfill@npm:1.2.6" + dependencies: + "@jsdotlua/boolean": "npm:^1.2.6" + "@jsdotlua/collections": "npm:^1.2.6" + "@jsdotlua/console": "npm:^1.2.6" + "@jsdotlua/es7-types": "npm:^1.2.6" + "@jsdotlua/instance-of": "npm:^1.2.6" + "@jsdotlua/math": "npm:^1.2.6" + "@jsdotlua/number": "npm:^1.2.6" + "@jsdotlua/string": "npm:^1.2.6" + "@jsdotlua/timers": "npm:^1.2.6" + symbol-luau: "npm:^1.0.0" + checksum: 63265f9fde3b895400d12802929ddab5daed79efc4e2aa90325b2eea0e2b7addcf351d35ad8bb19955956e913c212408944d73b6f3126223893d17cecd24de61 + languageName: node + linkType: hard + +"@jsdotlua/math@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/math@npm:1.2.6" + checksum: 06137df2a6352d4f5c730a7cf9e2902509d36de08415a3c70d470a05669dfeaf48e4aff2d5b6885a92b8f4d19fd0294a40f147a3f39c8558e887a09eb3ccd9e0 + languageName: node + linkType: hard + +"@jsdotlua/number@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/number@npm:1.2.6" + checksum: 74541fce86f80c52f05e5d4ca774d94f08ed21619af7b575c2bb7284e24fc2b178626c9888e286459a344d662cafab74d839ec8b95dcdab551fd14754911a292 + languageName: node + linkType: hard + +"@jsdotlua/react@npm:^17.1.0": + version: 17.1.0 + resolution: "@jsdotlua/react@npm:17.1.0" + dependencies: + "@jsdotlua/luau-polyfill": "npm:^1.2.6" + "@jsdotlua/shared": "npm:^17.1.0" + checksum: e25ba407bcd5f9395e6c206dfcbbd2b5e53c349046363830cb1a5e65c77a7978c8c680acf8c0e3d3d50bbf91b70796380b1bc43134b7d330303bef358afc5865 + languageName: node + linkType: hard + +"@jsdotlua/shared@npm:^17.1.0": + version: 17.1.0 + resolution: "@jsdotlua/shared@npm:17.1.0" + dependencies: + "@jsdotlua/luau-polyfill": "npm:^1.2.6" + checksum: 23e0d4fec494e7059b116db3b89e9655cc772e19fe06f9ad92bc97a324cf8862b0f2e7aaffee89d0627c9a1abb793b254573f09860fa35caf21925a33b332376 + languageName: node + linkType: hard + +"@jsdotlua/string@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/string@npm:1.2.6" + dependencies: + "@jsdotlua/es7-types": "npm:^1.2.6" + "@jsdotlua/number": "npm:^1.2.6" + checksum: de51c662439110642b699e4196240aa45ec4b8814f99bc606acb84dbb3f1c6889099bb722b3370f5873e06bd65c76779b5d94694309c9bdc63d324f40add2b0f + languageName: node + linkType: hard + +"@jsdotlua/timers@npm:^1.2.6": + version: 1.2.6 + resolution: "@jsdotlua/timers@npm:1.2.6" + dependencies: + "@jsdotlua/collections": "npm:^1.2.6" + checksum: f3ec2753894e4939f7dfdf0072517f1e769e4009b4bd882c9c6878526372753ac4b08b531e1a32c96b7bccb5bf10bb8e2f5730cbc70534da7eddc620fbccaaca + languageName: node + linkType: hard + +"@seaofvoices/react-lua-hooks@workspace:packages/react-lua-hooks": + version: 0.0.0-use.local + resolution: "@seaofvoices/react-lua-hooks@workspace:packages/react-lua-hooks" + dependencies: + "@jsdotlua/react": "npm:^17.1.0" + "@seaofvoices/react-lua-use-constant": "workspace:^" + luau-disk: "npm:^0.1.0" + npmluau: "npm:^0.1.1" + languageName: unknown + linkType: soft + +"@seaofvoices/react-lua-use-constant@workspace:^, @seaofvoices/react-lua-use-constant@workspace:packages/react-lua-use-constant": + version: 0.0.0-use.local + resolution: "@seaofvoices/react-lua-use-constant@workspace:packages/react-lua-use-constant" + dependencies: + "@jsdotlua/react": "npm:^17.1.0" + npmluau: "npm:^0.1.1" + languageName: unknown + linkType: soft + +"@seaofvoices/react-roblox-hooks@workspace:packages/react-roblox-hooks": + version: 0.0.0-use.local + resolution: "@seaofvoices/react-roblox-hooks@workspace:packages/react-roblox-hooks" + dependencies: + "@jsdotlua/react": "npm:^17.1.0" + "@seaofvoices/react-lua-use-constant": "workspace:^" + "@seaofvoices/react-roblox-use-service": "workspace:^" + luau-disk: "npm:^0.1.0" + npmluau: "npm:^0.1.1" + languageName: unknown + linkType: soft + +"@seaofvoices/react-roblox-use-service@workspace:^, @seaofvoices/react-roblox-use-service@workspace:packages/react-roblox-use-service": + version: 0.0.0-use.local + resolution: "@seaofvoices/react-roblox-use-service@workspace:packages/react-roblox-use-service" + dependencies: + "@jsdotlua/react": "npm:^17.1.0" + npmluau: "npm:^0.1.1" + languageName: unknown + linkType: soft + +"commander@npm:^11.0.0": + version: 11.1.0 + resolution: "commander@npm:11.1.0" + checksum: 13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179 + languageName: node + linkType: hard + +"luau-disk@npm:^0.1.0": + version: 0.1.0 + resolution: "luau-disk@npm:0.1.0" + checksum: ae5b5f6d4ab4c1fd8385236f511cf68f9c34f4f82d65f2c6beae33874b5f781401d4f1439c8ce682ab0db4ee205e638d81a300408ffe5a22305804a4d71acbc4 + languageName: node + linkType: hard + +"npmluau@npm:^0.1.1": + version: 0.1.1 + resolution: "npmluau@npm:0.1.1" + dependencies: + commander: "npm:^11.0.0" + walkdir: "npm:^0.4.1" + bin: + npmluau: main.js + checksum: 9ae22c0dcff9e85c90b4da4e8c17bc51e9b567b4a417c9767d355ff68faca4f99a2934b581743ebc8729f6851d1ba5b64597312151747252e040517d1794fbca + languageName: node + linkType: hard + +"symbol-luau@npm:^1.0.0": + version: 1.0.1 + resolution: "symbol-luau@npm:1.0.1" + checksum: ab51a77331b2d5e4666528bada17e67b26aea355257bba9e97351016cd1836bd19f372355a14cf5bef2f4d5bc6b32fe91aeb09698d7bdc079d2c61330bedf251 + languageName: node + linkType: hard + +"walkdir@npm:^0.4.1": + version: 0.4.1 + resolution: "walkdir@npm:0.4.1" + checksum: 88e635aa9303e9196e4dc15013d2bd4afca4c8c8b4bb27722ca042bad213bb882d3b9141b3b0cca6bfb274f7889b30cf58d6374844094abec0016f335c5414dc + languageName: node + linkType: hard + +"workspace@workspace:.": + version: 0.0.0-use.local + resolution: "workspace@workspace:." + dependencies: + npmluau: "npm:^0.1.1" + languageName: unknown + linkType: soft