From 95476e93ec6752ac4c6f0a09ce341cc07b35f6d7 Mon Sep 17 00:00:00 2001 From: littensy <56808540+littensy@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:25:50 -0700 Subject: [PATCH] refactor: improve structure and alien-signals integration --- .eslintrc | 28 + .gitignore | 2 + .luaurc | 4 - .prettierrc | 6 + benches/mapped.luau | 4 +- benches/patch.bench.luau | 4 +- benches/propagate.luau | 31 +- bin/build.sh | 7 +- bin/check.sh | 9 +- build.project.json | 10 + default.project.json | 59 +- package.json | 23 +- packages/charm-sync/Charm.luau | 6 + packages/charm-sync/default.project.json | 2 +- packages/charm-sync/package.json | 27 + .../{tests => src/__tests__}/client.spec.luau | 9 +- .../__tests__}/flatten.spec.luau | 9 +- .../{tests => src/__tests__}/patch.spec.luau | 6 +- .../{tests => src/__tests__}/server.spec.luau | 9 +- .../{tests => src/__tests__}/spec.luau | 7 +- .../__tests__}/validate.spec.luau | 9 +- packages/charm-sync/src/client.luau | 12 +- packages/charm-sync/src/flatten.luau | 2 +- packages/charm-sync/src/index.d.ts | 1 + packages/charm-sync/src/init.luau | 12 +- packages/charm-sync/src/interval.luau | 4 +- packages/charm-sync/src/patch.luau | 7 +- packages/charm-sync/src/server.luau | 28 +- packages/charm-sync/src/types.luau | 2 +- packages/charm-sync/wally.toml | 8 +- packages/charm/default.project.json | 2 +- packages/charm/package.json | 24 + .../__tests__/core}/computed.spec.luau | 32 +- .../__tests__/core}/effect.spec.luau | 118 +- .../__tests__/core}/effectScope.spec.luau | 31 +- .../__tests__/core}/gc.spec.luau | 26 +- .../__tests__/core}/issue_48.spec.luau | 20 +- .../__tests__/core}/topology.spec.luau | 82 +- .../__tests__/core}/untrack.spec.luau | 14 +- .../{tests => src/__tests__}/listen.spec.luau | 4 +- .../{tests => src/__tests__}/mapped.spec.luau | 4 +- .../__tests__}/observe.spec.luau | 4 +- packages/charm/src/__tests__/spec.luau | 45 + .../__tests__}/subscribe.spec.luau | 4 +- .../__tests__}/untracked.spec.luau | 4 +- packages/charm/src/atom.luau | 48 - packages/charm/src/batched.luau | 10 +- .../charm/src/{signals.luau => core.luau} | 377 ++-- packages/charm/src/effect.luau | 26 - packages/charm/src/effectScope.luau | 14 - packages/charm/src/index.d.ts | 1 + packages/charm/src/init.luau | 21 +- packages/charm/src/listen.luau | 16 +- packages/charm/src/mapped.luau | 27 +- packages/charm/src/observe.luau | 20 +- packages/charm/src/onCleanup.luau | 20 - packages/charm/src/strict.luau | 8 + packages/charm/src/subscribe.luau | 19 +- packages/charm/src/system.luau | 20 +- packages/charm/src/types.luau | 12 +- packages/charm/src/untracked.luau | 15 +- .../charm/src/{noYield.luau => utils.luau} | 14 +- packages/charm/test/default.project.json | 6 + packages/charm/test/src/batched.luau | 20 + packages/charm/test/src/core.luau | 445 +++++ packages/charm/test/src/init.luau | 34 + packages/charm/test/src/listen.luau | 32 + packages/charm/test/src/mapped.luau | 59 + packages/charm/test/src/observe.luau | 52 + packages/charm/test/src/strict.luau | 8 + packages/charm/test/src/subscribe.luau | 26 + packages/charm/test/src/system.luau | 357 ++++ packages/charm/test/src/types.luau | 5 + packages/charm/test/src/untracked.luau | 18 + packages/charm/test/src/utils.luau | 18 + packages/charm/test/wally.toml | 8 + packages/charm/tests/signals/spec.luau | 27 - packages/charm/tests/spec.luau | 23 - packages/charm/wally.toml | 2 +- packages/react-charm/Charm.luau | 6 + packages/react-charm/React.luau | 12 + packages/react-charm/ReactRoblox.luau | 1 + packages/react-charm/default.project.json | 2 +- packages/react-charm/package.json | 30 + .../__tests__}/helpers/renderHook.luau | 6 +- .../{tests => src/__tests__}/spec.luau | 7 +- .../__tests__}/useAtom.spec.luau | 11 +- .../__tests__}/useAtomBinding.spec.luau | 11 +- packages/react-charm/src/index.d.ts | 1 + packages/react-charm/src/init.luau | 4 +- packages/react-charm/wally.toml | 9 +- packages/vide-charm/Charm.luau | 6 + packages/vide-charm/Vide.luau | 1 + packages/vide-charm/default.project.json | 2 +- packages/vide-charm/package.json | 30 + .../{tests => src/__tests__}/spec.luau | 7 +- .../__tests__}/useAtom.spec.luau | 11 +- packages/vide-charm/src/index.d.ts | 1 + packages/vide-charm/src/init.luau | 4 +- packages/vide-charm/wally.toml | 9 +- pnpm-lock.yaml | 1679 +++++++++++++++++ pnpm-workspace.yaml | 2 + rokit.toml | 5 +- selene.toml | 1 + test/init.luau | 9 - test/spec.server.luau | 19 +- tsconfig.json | 20 + 107 files changed, 3660 insertions(+), 815 deletions(-) create mode 100644 .eslintrc create mode 100644 .prettierrc create mode 100644 build.project.json create mode 100644 packages/charm-sync/Charm.luau create mode 100644 packages/charm-sync/package.json rename packages/charm-sync/{tests => src/__tests__}/client.spec.luau (89%) rename packages/charm-sync/{tests => src/__tests__}/flatten.spec.luau (66%) rename packages/charm-sync/{tests => src/__tests__}/patch.spec.luau (94%) rename packages/charm-sync/{tests => src/__tests__}/server.spec.luau (89%) rename packages/charm-sync/{tests => src/__tests__}/spec.luau (76%) rename packages/charm-sync/{tests => src/__tests__}/validate.spec.luau (76%) create mode 100644 packages/charm-sync/src/index.d.ts create mode 100644 packages/charm/package.json rename packages/charm/{tests/signals => src/__tests__/core}/computed.spec.luau (67%) rename packages/charm/{tests/signals => src/__tests__/core}/effect.spec.luau (80%) rename packages/charm/{tests/signals => src/__tests__/core}/effectScope.spec.luau (73%) rename packages/charm/{tests/signals => src/__tests__/core}/gc.spec.luau (70%) rename packages/charm/{tests/signals => src/__tests__/core}/issue_48.spec.luau (87%) rename packages/charm/{tests/signals => src/__tests__/core}/topology.spec.luau (89%) rename packages/charm/{tests/signals => src/__tests__/core}/untrack.spec.luau (57%) rename packages/charm/{tests => src/__tests__}/listen.spec.luau (96%) rename packages/charm/{tests => src/__tests__}/mapped.spec.luau (97%) rename packages/charm/{tests => src/__tests__}/observe.spec.luau (94%) create mode 100644 packages/charm/src/__tests__/spec.luau rename packages/charm/{tests => src/__tests__}/subscribe.spec.luau (97%) rename packages/charm/{tests => src/__tests__}/untracked.spec.luau (95%) delete mode 100644 packages/charm/src/atom.luau rename packages/charm/src/{signals.luau => core.luau} (52%) delete mode 100644 packages/charm/src/effect.luau delete mode 100644 packages/charm/src/effectScope.luau create mode 100644 packages/charm/src/index.d.ts delete mode 100644 packages/charm/src/onCleanup.luau create mode 100644 packages/charm/src/strict.luau rename packages/charm/src/{noYield.luau => utils.luau} (62%) create mode 100644 packages/charm/test/default.project.json create mode 100644 packages/charm/test/src/batched.luau create mode 100644 packages/charm/test/src/core.luau create mode 100644 packages/charm/test/src/init.luau create mode 100644 packages/charm/test/src/listen.luau create mode 100644 packages/charm/test/src/mapped.luau create mode 100644 packages/charm/test/src/observe.luau create mode 100644 packages/charm/test/src/strict.luau create mode 100644 packages/charm/test/src/subscribe.luau create mode 100644 packages/charm/test/src/system.luau create mode 100644 packages/charm/test/src/types.luau create mode 100644 packages/charm/test/src/untracked.luau create mode 100644 packages/charm/test/src/utils.luau create mode 100644 packages/charm/test/wally.toml delete mode 100644 packages/charm/tests/signals/spec.luau delete mode 100644 packages/charm/tests/spec.luau create mode 100644 packages/react-charm/Charm.luau create mode 100644 packages/react-charm/React.luau create mode 100644 packages/react-charm/ReactRoblox.luau create mode 100644 packages/react-charm/package.json rename packages/react-charm/{tests => src/__tests__}/helpers/renderHook.luau (79%) rename packages/react-charm/{tests => src/__tests__}/spec.luau (63%) rename packages/react-charm/{tests => src/__tests__}/useAtom.spec.luau (72%) rename packages/react-charm/{tests => src/__tests__}/useAtomBinding.spec.luau (74%) create mode 100644 packages/react-charm/src/index.d.ts create mode 100644 packages/vide-charm/Charm.luau create mode 100644 packages/vide-charm/Vide.luau create mode 100644 packages/vide-charm/package.json rename packages/vide-charm/{tests => src/__tests__}/spec.luau (51%) rename packages/vide-charm/{tests => src/__tests__}/useAtom.spec.luau (64%) create mode 100644 packages/vide-charm/src/index.d.ts create mode 100644 pnpm-workspace.yaml delete mode 100644 test/init.luau create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..5fe4250 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,28 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "jsx": true, + "useJSXTextNode": true, + "ecmaVersion": 2018, + "sourceType": "module", + "project": [ + "./tsconfig.json", + "./examples/ts/tsconfig.json" + ] + }, + "plugins": [ + "@typescript-eslint", + "roblox-ts", + "prettier" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:roblox-ts/recommended", + "plugin:prettier/recommended" + ], + "rules": { + "prettier/prettier": "warn", + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/.gitignore b/.gitignore index b3bbbd9..815441d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /.pesde /build /packages/**/wally.lock +/packages/**/LICENSE.md +/packages/**/README.md /sourcemap.json node_modules *.tsbuildinfo diff --git a/.luaurc b/.luaurc index e03053a..4c8043d 100644 --- a/.luaurc +++ b/.luaurc @@ -2,9 +2,5 @@ "languageMode": "strict", "lint": { "LocalShadow": false - }, - "aliases": { - "test": "./test", - "packages": "./packages" } } diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..537ac33 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "trailingComma": "all", + "useTabs": true +} diff --git a/benches/mapped.luau b/benches/mapped.luau index 9fe3ae1..539310c 100644 --- a/benches/mapped.luau +++ b/benches/mapped.luau @@ -1,10 +1,10 @@ -local Charm = require("@packages/charm/src") +local Charm = require("../packages/charm/src") local bench = require("./bench") local atom = Charm.atom local computed = Charm.computed local mapped = Charm.mapped -local setCurrentSub = Charm.signals.setCurrentSub +local setCurrentSub = Charm.core.setCurrentSub type Entry = { number } diff --git a/benches/patch.bench.luau b/benches/patch.bench.luau index 8d4eeea..a4c5829 100644 --- a/benches/patch.bench.luau +++ b/benches/patch.bench.luau @@ -1,6 +1,4 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") - -local CharmSync = require(ReplicatedStorage.Packages.CharmSync) +local CharmSync = require("../packages/charm-sync/src") local patch = CharmSync.patch diff --git a/benches/propagate.luau b/benches/propagate.luau index c31cf52..9d40d9b 100644 --- a/benches/propagate.luau +++ b/benches/propagate.luau @@ -1,42 +1,27 @@ -local Charm = require("@packages/charm/src") +local Charm = require("../packages/charm/src") local bench = require("./bench") -local signals = Charm.signals +local atom = Charm.atom +local computed = Charm.computed +local effect = Charm.effect for _, w in { 10, 100 } do for _, h in { 10, 100 } do - bench.run(`charm {w}*{h}`, function() - local src = Charm.atom(1) + bench.run(`propagate {w}*{h}`, function() + local src = atom(1) for _ = 1, w do local last = src for _ = 1, h do local prev = last - last = Charm.computed(function() + last = computed(function() return prev() + 1 end) end - Charm.effect(function() + effect(function() last() end) end src(src() + 1) end) - - bench.run(`alien-signals {w}*{h}`, function() - local src, setSrc = signals.signal(1) - for _ = 1, w do - local last = src - for _ = 1, h do - local prev = last - last = signals.computed(function() - return prev() + 1 - end) - end - signals.effect(function() - last() - end) - end - setSrc(src() + 1) - end) end end diff --git a/bin/build.sh b/bin/build.sh index be6fdf7..f680537 100644 --- a/bin/build.sh +++ b/bin/build.sh @@ -1,5 +1,4 @@ +#!/usr/bin/env bash + mkdir build -rojo build packages/charm -o build/charm.rbxm -rojo build packages/charm-sync -o build/charm-sync.rbxm -rojo build packages/react-charm -o build/react-charm.rbxm -rojo build packages/vide-charm -o build/vide-charm.rbxm +rojo build build.project.json -o build/charm.rbxm diff --git a/bin/check.sh b/bin/check.sh index 25cd88d..091c4a4 100644 --- a/bin/check.sh +++ b/bin/check.sh @@ -5,12 +5,15 @@ rojo sourcemap -o sourcemap.json luau-lsp analyze \ --defs=bin/roblox.d.luau \ --flag:LuauFixIndexerSubtypingOrdering=true \ + --flag:LuauInstantiateInSubtyping=true \ --sourcemap=sourcemap.json \ --ignore="**/node_modules/**" \ - packages test + packages test benches -selene packages test +selene packages test benches -stylua --check packages test +stylua --check packages test benches + +pnpm eslint packages rm bin/roblox.d.luau diff --git a/build.project.json b/build.project.json new file mode 100644 index 0000000..8141b86 --- /dev/null +++ b/build.project.json @@ -0,0 +1,10 @@ +{ + "name": "Packages", + "tree": { + "$className": "Folder", + "Charm": { "$path": "packages/charm/src" }, + "CharmSync": { "$path": "packages/charm-sync/src" }, + "ReactCharm": { "$path": "packages/react-charm/src" }, + "VideCharm": { "$path": "packages/vide-charm/src" } + } +} diff --git a/default.project.json b/default.project.json index f40f123..43a7d1a 100644 --- a/default.project.json +++ b/default.project.json @@ -1,29 +1,50 @@ { "name": "charm-test", + "globIgnorePaths": [ + "**/default.project.json", + "node_modules/charm", + "node_modules/charm-sync", + "node_modules/react-charm", + "node_modules/vide-charm" + ], + "emitLegacyScripts": false, "tree": { "$className": "DataModel", "ReplicatedStorage": { "$className": "ReplicatedStorage", - "Packages": { - "$path": "node_modules/@rbxts-js", - "Vide": { "$path": "node_modules/@rbxts/vide/src" }, - "Charm": { "$path": "packages/charm" }, - "CharmSync": { "$path": "packages/charm-sync" }, - "ReactCharm": { "$path": "packages/react-charm" }, - "VideCharm": { "$path": "packages/vide-charm" } - } - }, - "ServerScriptService": { - "$className": "ServerScriptService", - "Tests": { - "$path": "test", - "Charm": { "$path": "packages/charm/tests" }, - "CharmSync": { "$path": "packages/charm-sync/tests" }, - "ReactCharm": { "$path": "packages/react-charm/tests" }, - "VideCharm": { "$path": "packages/vide-charm/tests" } + "packages": { + "$className": "Folder", + "vide": { + "$path": "node_modules/@rbxts/vide" + }, + "charm": { + "$className": "Folder", + "src": { "$path": "packages/charm/src" } + }, + "charm-sync": { + "$className": "Folder", + "src": { "$path": "packages/charm-sync/src" }, + "Charm": { "$path": "packages/charm-sync/Charm.luau" } + }, + "react-charm": { + "$className": "Folder", + "src": { "$path": "packages/react-charm/src" }, + "React": { "$path": "packages/react-charm/React.luau" }, + "ReactRoblox": { "$path": "packages/react-charm/ReactRoblox.luau" }, + "Charm": { "$path": "packages/react-charm/Charm.luau" } + }, + "vide-charm": { + "$className": "Folder", + "src": { "$path": "packages/vide-charm/src" }, + "Vide": { "$path": "packages/vide-charm/Vide.luau" }, + "Charm": { "$path": "packages/vide-charm/Charm.luau" } + } }, - "Benchmarks": { - "$path": "benches" + "@rbxts-js": { + "$path": "node_modules/@rbxts-js" + }, + "test": { + "$path": "test" } } } diff --git a/package.json b/package.json index 8e29b88..49c9f01 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,29 @@ { "private": true, "scripts": { - "test": "lune run test", + "test": "lune run test/spec.server.luau", "build": "sh bin/build.sh", - "typecheck": "sh bin/check.sh" + "check": "sh bin/check.sh", + "publish:npm": "pnpm publish -r --access public", + "publish:wally": "pnpm -r --workspace-concurrency 1 publish:wally" }, - "packageManager": "pnpm@10.11.0", + "packageManager": "pnpm@10.12.4", "devDependencies": { "@rbxts-js/react": "17.1.20-ts.1", "@rbxts-js/react-roblox": "17.1.20-ts.1", - "@rbxts/vide": "^0.5.7" + "@rbxts/compiler-types": "3.0.0-types.0", + "@rbxts/react": "17.3.0-alpha.1", + "@rbxts/react-roblox": "17.3.0-alpha.1", + "@rbxts/types": "^1.0.861", + "@rbxts/vide": "^0.5.7", + "@typescript-eslint/eslint-plugin": "^8.35.1", + "@typescript-eslint/parser": "^8.35.1", + "eslint": "^8.57.1", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-roblox-ts": "^0.0.36", + "prettier": "^3.6.2", + "roblox-ts": "^3.0.0", + "typescript": "^5.8.3" } } diff --git a/packages/charm-sync/Charm.luau b/packages/charm-sync/Charm.luau new file mode 100644 index 0000000..6b6fb92 --- /dev/null +++ b/packages/charm-sync/Charm.luau @@ -0,0 +1,6 @@ +local Charm = require(script.Parent.Parent.charm.src) + +export type Atom = Charm.Atom +export type Selector = Charm.Selector + +return Charm diff --git a/packages/charm-sync/default.project.json b/packages/charm-sync/default.project.json index 5115ff0..9c44c7c 100644 --- a/packages/charm-sync/default.project.json +++ b/packages/charm-sync/default.project.json @@ -1,5 +1,5 @@ { - "name": "CharmSync", + "name": "charm-sync", "tree": { "$path": "src" } diff --git a/packages/charm-sync/package.json b/packages/charm-sync/package.json new file mode 100644 index 0000000..129cf4a --- /dev/null +++ b/packages/charm-sync/package.json @@ -0,0 +1,27 @@ +{ + "name": "@rbxts/charm-sync", + "version": "0.3.0", + "description": "Simple server-to-client sync for Charm", + "license": "MIT", + "main": "src/init.luau", + "types": "src/index.d.ts", + "files": [ + "src", + "*.luau", + "!wally.toml", + "!wally.lock", + "!default.project.json", + "!**/__tests__" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/littensy/charm.git" + }, + "scripts": { + "prepare": "cp ../../LICENSE.md . && cp ../../README.md .", + "publish:wally": "wally publish" + }, + "dependencies": { + "@rbxts/charm": "workspace:*" + } +} diff --git a/packages/charm-sync/tests/client.spec.luau b/packages/charm-sync/src/__tests__/client.spec.luau similarity index 89% rename from packages/charm-sync/tests/client.spec.luau rename to packages/charm-sync/src/__tests__/client.spec.luau index d335660..eaebab2 100644 --- a/packages/charm-sync/tests/client.spec.luau +++ b/packages/charm-sync/src/__tests__/client.spec.luau @@ -1,9 +1,6 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") - -local Charm = require(ReplicatedStorage.Packages.Charm) -local CharmSync = require(ReplicatedStorage.Packages.CharmSync) -local suite = require(ServerScriptService.Tests.suite) +local Charm = require("../../Charm") +local CharmSync = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local subscribe = Charm.subscribe diff --git a/packages/charm-sync/tests/flatten.spec.luau b/packages/charm-sync/src/__tests__/flatten.spec.luau similarity index 66% rename from packages/charm-sync/tests/flatten.spec.luau rename to packages/charm-sync/src/__tests__/flatten.spec.luau index 1b1963f..2507b17 100644 --- a/packages/charm-sync/tests/flatten.spec.luau +++ b/packages/charm-sync/src/__tests__/flatten.spec.luau @@ -1,9 +1,6 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") - -local Charm = require(ReplicatedStorage.Packages.Charm) -local CharmSync = require(ReplicatedStorage.Packages.CharmSync) -local suite = require(ServerScriptService.Tests.suite) +local Charm = require("../../Charm") +local CharmSync = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local flatten = CharmSync.flatten diff --git a/packages/charm-sync/tests/patch.spec.luau b/packages/charm-sync/src/__tests__/patch.spec.luau similarity index 94% rename from packages/charm-sync/tests/patch.spec.luau rename to packages/charm-sync/src/__tests__/patch.spec.luau index a38cc4a..05bfef5 100644 --- a/packages/charm-sync/tests/patch.spec.luau +++ b/packages/charm-sync/src/__tests__/patch.spec.luau @@ -1,9 +1,7 @@ local HttpService = game:GetService("HttpService") -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") -local CharmSync = require(ReplicatedStorage.Packages.CharmSync) -local suite = require(ServerScriptService.Tests.suite) +local CharmSync = require("../") +local suite = require("../../../../test/suite") local patch = CharmSync.patch local test = suite.test diff --git a/packages/charm-sync/tests/server.spec.luau b/packages/charm-sync/src/__tests__/server.spec.luau similarity index 89% rename from packages/charm-sync/tests/server.spec.luau rename to packages/charm-sync/src/__tests__/server.spec.luau index 4a51fb0..d88b36f 100644 --- a/packages/charm-sync/tests/server.spec.luau +++ b/packages/charm-sync/src/__tests__/server.spec.luau @@ -1,9 +1,6 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") - -local Charm = require(ReplicatedStorage.Packages.Charm) -local CharmSync = require(ReplicatedStorage.Packages.CharmSync) -local suite = require(ServerScriptService.Tests.suite) +local Charm = require("../../Charm") +local CharmSync = require("../") +local suite = require("../../../../test/suite") type SyncPayload = CharmSync.SyncPayload diff --git a/packages/charm-sync/tests/spec.luau b/packages/charm-sync/src/__tests__/spec.luau similarity index 76% rename from packages/charm-sync/tests/spec.luau rename to packages/charm-sync/src/__tests__/spec.luau index 725714b..10f3bd9 100644 --- a/packages/charm-sync/tests/spec.luau +++ b/packages/charm-sync/src/__tests__/spec.luau @@ -1,8 +1,9 @@ -local ServerScriptService = game:GetService("ServerScriptService") - -local suite = require(ServerScriptService.Tests.suite) +local suite = require("../../../../test/suite") suite.describe("charm-sync", function() + if not game then + return suite.skip() + end suite.describe("client", function() require("./client.spec") end) diff --git a/packages/charm-sync/tests/validate.spec.luau b/packages/charm-sync/src/__tests__/validate.spec.luau similarity index 76% rename from packages/charm-sync/tests/validate.spec.luau rename to packages/charm-sync/src/__tests__/validate.spec.luau index 87b048a..86d2edb 100644 --- a/packages/charm-sync/tests/validate.spec.luau +++ b/packages/charm-sync/src/__tests__/validate.spec.luau @@ -1,9 +1,6 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") - -local Charm = require(ReplicatedStorage.Packages.Charm) -local CharmSync = require(ReplicatedStorage.Packages.CharmSync) -local suite = require(ServerScriptService.Tests.suite) +local Charm = require("../../Charm") +local CharmSync = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local validate = CharmSync.validate diff --git a/packages/charm-sync/src/client.luau b/packages/charm-sync/src/client.luau index a36c55b..3bb778d 100644 --- a/packages/charm-sync/src/client.luau +++ b/packages/charm-sync/src/client.luau @@ -1,6 +1,6 @@ -local Charm = require(script.Parent.Parent.Charm) -local patch = require(script.Parent.patch) -local types = require(script.Parent.types) +local Charm = require("../Charm") +local patch = require("./patch") +local types = require("./types") type AtomMap = types.AtomMap type SyncPayload = types.SyncPayload @@ -36,7 +36,7 @@ local function client(options: ClientOptions): ClientSyncer local function hydrate(state: { [string | number]: any }) hydrated = true - for key, value in next, state do + for key, value in state do local atom = atoms[key] atom(value) end @@ -45,13 +45,13 @@ local function client(options: ClientOptions): ClientSyncer local function apply(data: { [string | number]: any }) local target = {} - for key, atom in next, atoms do + for key, atom in atoms do target[key] = atom() end target = patch.apply(target, data) - for key, atom in next, atoms do + for key, atom in atoms do atom(target[key]) end end diff --git a/packages/charm-sync/src/flatten.luau b/packages/charm-sync/src/flatten.luau index 42a93eb..5d4a60d 100644 --- a/packages/charm-sync/src/flatten.luau +++ b/packages/charm-sync/src/flatten.luau @@ -1,4 +1,4 @@ -local types = require(script.Parent.types) +local types = require("./types") type AtomMap = types.AtomMap diff --git a/packages/charm-sync/src/index.d.ts b/packages/charm-sync/src/index.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/charm-sync/src/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/charm-sync/src/init.luau b/packages/charm-sync/src/init.luau index 0978041..249922f 100644 --- a/packages/charm-sync/src/init.luau +++ b/packages/charm-sync/src/init.luau @@ -1,9 +1,9 @@ -local client = require(script.client) -local flatten = require(script.flatten) -local patch = require(script.patch) -local server = require(script.server) -local types = require(script.types) -local validate = require(script.validate) +local client = require("@self/client") +local flatten = require("@self/flatten") +local patch = require("@self/patch") +local server = require("@self/server") +local types = require("@self/types") +local validate = require("@self/validate") export type SyncPayload = types.SyncPayload diff --git a/packages/charm-sync/src/interval.luau b/packages/charm-sync/src/interval.luau index 7788d41..1225ecb 100644 --- a/packages/charm-sync/src/interval.luau +++ b/packages/charm-sync/src/interval.luau @@ -1,5 +1,3 @@ -local RunService = game:GetService("RunService") - --- Schedules a function to be called at a fixed interval of `seconds`. --- --- @param callback The function to call. @@ -12,7 +10,7 @@ local function interval(callback: () -> (), seconds: number): () -> () local clock = 0 - local connection = RunService.Heartbeat:Connect(function(delta) + local connection = game:GetService("RunService").Heartbeat:Connect(function(delta) clock += delta if clock >= seconds then clock = 0 diff --git a/packages/charm-sync/src/patch.luau b/packages/charm-sync/src/patch.luau index 4d87738..f96f6f6 100644 --- a/packages/charm-sync/src/patch.luau +++ b/packages/charm-sync/src/patch.luau @@ -1,6 +1,5 @@ -local validate = require(script.Parent.validate) - -local __DEV__ = _G.__DEV__ +local Charm = require("../Charm") +local validate = require("./validate") --- A special symbol that denotes the absence of a value. Used to represent --- deleted values in patches. @@ -64,7 +63,7 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serial patches = stringifySparseArray(patches) end - if serialize and __DEV__ then + if serialize and Charm.strict.enabled then for key, value in next, patches do validate(value, key) end diff --git a/packages/charm-sync/src/server.luau b/packages/charm-sync/src/server.luau index 80fac19..880944a 100644 --- a/packages/charm-sync/src/server.luau +++ b/packages/charm-sync/src/server.luau @@ -1,9 +1,7 @@ -local Players = game:GetService("Players") - -local Charm = require(script.Parent.Parent.Charm) -local interval = require(script.Parent.interval) -local patch = require(script.Parent.patch) -local types = require(script.Parent.types) +local Charm = require("../Charm") +local interval = require("./interval") +local patch = require("./patch") +local types = require("./types") type AtomMap = types.AtomMap type SyncPayload = types.SyncPayload @@ -64,7 +62,7 @@ local function server(options: ServerOptions): ServerSyncer local function createSnapshot() local snapshot = {} - for key, atom in next, atoms do + for key, atom in atoms do snapshot[key] = atom() end @@ -110,7 +108,7 @@ local function server(options: ServerOptions): ServerSyncer -- Populate the snapshot with the initial state of each atom. -- Subscribe to each atom and update the state when it changes. - for key, atom in next, atoms do + for key, atom in atoms do subscriptions[key] = Charm.subscribe(atom, function(current, previous) pushSnapshot(key, current, previous) changed = true @@ -125,7 +123,7 @@ local function server(options: ServerOptions): ServerSyncer local payloads: { SyncPayload } = {} local lastSnapshot - for index, snapshot in next, snapshots do + for index, snapshot in snapshots do lastSnapshot = snapshot if index == 1 then @@ -141,7 +139,7 @@ local function server(options: ServerOptions): ServerSyncer snapshots = { lastSnapshot } changed = false - for _, player in next, Players:GetPlayers() do + for _, player in game:GetService("Players"):GetPlayers() do callback(player, unpack(payloads)) end end, syncRate) @@ -149,7 +147,7 @@ local function server(options: ServerOptions): ServerSyncer return function() disconnect() - for _, unsubscribe in next, subscriptions do + for _, unsubscribe in subscriptions do unsubscribe() end end @@ -164,7 +162,7 @@ local function server(options: ServerOptions): ServerSyncer local payloads: { SyncPayload } = {} - for index, snapshot in next, snapshots do + for index, snapshot in snapshots do if index == 1 then continue end @@ -194,7 +192,7 @@ local function server(options: ServerOptions): ServerSyncer sync = callback -- Subscribe to each atom and update the state when one changes. - for key, atom in next, atoms do + for key, atom in atoms do subscriptions[key] = Charm.subscribe(atom, function(state) currentSnapshot[key] = state changed = true @@ -214,7 +212,7 @@ local function server(options: ServerOptions): ServerSyncer previousSnapshot = table.clone(currentSnapshot) changed = false - for _, player in next, Players:GetPlayers() do + for _, player in game:GetService("Players"):GetPlayers() do callback(player, payload) end end, syncRate) @@ -222,7 +220,7 @@ local function server(options: ServerOptions): ServerSyncer return function() disconnect() - for _, unsubscribe in next, subscriptions do + for _, unsubscribe in subscriptions do unsubscribe() end end diff --git a/packages/charm-sync/src/types.luau b/packages/charm-sync/src/types.luau index e29bd62..791707f 100644 --- a/packages/charm-sync/src/types.luau +++ b/packages/charm-sync/src/types.luau @@ -1,4 +1,4 @@ -local Charm = require(script.Parent.Parent.Charm) +local Charm = require("../Charm") type Atom = Charm.Atom diff --git a/packages/charm-sync/wally.toml b/packages/charm-sync/wally.toml index 41d31ee..c5fc5a0 100644 --- a/packages/charm-sync/wally.toml +++ b/packages/charm-sync/wally.toml @@ -3,7 +3,13 @@ name = "littensy/charm-sync" version = "0.3.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" -exclude = ["tests"] +exclude = [ + "node_modules", + "src/__tests__", + "src/index.d.ts", + "package.json", + "Charm.luau", +] [dependencies] Charm = "littensy/charm@^0.10" diff --git a/packages/charm/default.project.json b/packages/charm/default.project.json index 89d3ada..a485e4d 100644 --- a/packages/charm/default.project.json +++ b/packages/charm/default.project.json @@ -1,5 +1,5 @@ { - "name": "Charm", + "name": "charm", "tree": { "$path": "src" } diff --git a/packages/charm/package.json b/packages/charm/package.json new file mode 100644 index 0000000..67c51c8 --- /dev/null +++ b/packages/charm/package.json @@ -0,0 +1,24 @@ +{ + "name": "@rbxts/charm", + "version": "0.10.0", + "description": "Atomic state management based on alien-signals", + "license": "MIT", + "main": "src/init.luau", + "types": "src/index.d.ts", + "files": [ + "src", + "*.luau", + "!wally.toml", + "!wally.lock", + "!default.project.json", + "!**/__tests__" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/littensy/charm.git" + }, + "scripts": { + "prepare": "cp ../../LICENSE.md . && cp ../../README.md .", + "publish:wally": "wally publish" + } +} diff --git a/packages/charm/tests/signals/computed.spec.luau b/packages/charm/src/__tests__/core/computed.spec.luau similarity index 67% rename from packages/charm/tests/signals/computed.spec.luau rename to packages/charm/src/__tests__/core/computed.spec.luau index 5c74738..c5f37d3 100644 --- a/packages/charm/tests/signals/computed.spec.luau +++ b/packages/charm/src/__tests__/core/computed.spec.luau @@ -1,12 +1,12 @@ -local signals = require("../../src/signals") -local suite = require("@test/suite") +local core = require("../../core") +local suite = require("../../../../../test/suite") -local computed = signals.computed -local signal = signals.signal +local computed = core.computed +local atom = core.atom local test = suite.test -test("should correctly propagate changes through computed signals", function() - local src, setSrc = signal(0) +test("should correctly propagate changes through computed core", function() + local src = atom(0) local c1 = computed(function() return src() % 2 end) @@ -18,15 +18,15 @@ test("should correctly propagate changes through computed signals", function() end) c3() - setSrc(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty + src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty c2() -- c1 -> none, c2 -> none - setSrc(3) -- c1 -> dirty, c2 -> toCheckDirty + src(3) -- c1 -> dirty, c2 -> toCheckDirty assert(c3() == 1, "bad final value") end) test("should propagate updated source value through chained computations", function() - local src, setSrc = signal(0) + local src = atom(0) local a = computed(function() return src() end) @@ -41,12 +41,12 @@ test("should propagate updated source value through chained computations", funct end) assert(d() == 0, "bad initial value") - setSrc(2) + src(2) assert(d() == 2, "bad updated value") end) test("should handle flags are indirectly updated during checkDirty", function() - local a, setA = signal(false) + local a = atom(false) local b = computed(function() return a() end) @@ -60,22 +60,22 @@ test("should handle flags are indirectly updated during checkDirty", function() end) assert(d() == false, "bad initial value") - setA(true) + a(true) assert(d() == true, "bad updated value") end) -test("should not update if the signal value is reverted", function() +test("should not update if the atom value is reverted", function() local times = 0 - local src, setSrc = signal(0) + local src = atom(0) local c1 = computed(function() times += 1 return src() end) c1() assert(times == 1, "computed did not run") - setSrc(1) - setSrc(0) + src(1) + src(0) c1() assert(times == 1, "computed ran when it should not") end) diff --git a/packages/charm/tests/signals/effect.spec.luau b/packages/charm/src/__tests__/core/effect.spec.luau similarity index 80% rename from packages/charm/tests/signals/effect.spec.luau rename to packages/charm/src/__tests__/core/effect.spec.luau index 58ad958..e02512a 100644 --- a/packages/charm/tests/signals/effect.spec.luau +++ b/packages/charm/src/__tests__/core/effect.spec.luau @@ -1,20 +1,20 @@ -local signals = require("../../src/signals") -local suite = require("@test/suite") +local core = require("../../core") +local suite = require("../../../../../test/suite") -local computed = signals.computed -local effect = signals.effect -local effectScope = signals.effectScope -local endBatch = signals.endBatch -local setCurrentSub = signals.setCurrentSub -local onEffectCleanup = signals.onEffectCleanup -local signal = signals.signal -local startBatch = signals.startBatch +local computed = core.computed +local effect = core.effect +local effectScope = core.effectScope +local endBatch = core.endBatch +local setCurrentSub = core.setCurrentSub +local onCleanup = core.onCleanup +local atom = core.atom +local startBatch = core.startBatch local test = suite.test test("should clear subscriptions when untracked by all subscribers", function() local bRunTimes = 0 - local a, setA = signal(1) + local a = atom(1) local b = computed(function() bRunTimes += 1 return a() * 2 @@ -24,15 +24,15 @@ test("should clear subscriptions when untracked by all subscribers", function() end) assert(bRunTimes == 1, "should run initially") - setA(2) + a(2) assert(bRunTimes == 2, "should run after change") stopEffect() - setA(3) + a(3) assert(bRunTimes == 2, "should not run after stop") end) test("should not run untracked inner effect", function() - local a, setA = signal(3) + local a = atom(3) local b = computed(function() return a() > 0 end) @@ -47,14 +47,14 @@ test("should not run untracked inner effect", function() end end) - setA(2) - setA(1) - setA(0) + a(2) + a(1) + a(0) end) test("should run outer effect first", function() - local a, setA = signal(1) - local b, setB = signal(1) + local a = atom(1) + local b = atom(1) effect(function() if a() > 0 then @@ -68,13 +68,13 @@ test("should run outer effect first", function() end) startBatch() - setB(0) - setA(0) + b(0) + a(0) endBatch() end) test("should not trigger inner effect when resolve maybe dirty", function() - local a, setA = signal(0) + local a = atom(0) local b = computed(function() return a() % 2 end) @@ -91,12 +91,12 @@ test("should not trigger inner effect when resolve maybe dirty", function() end) end) - setA(2) + a(2) end) test("should trigger inner effects in sequence", function() - local a, setA = signal(0) - local b, setB = signal(0) + local a = atom(0) + local b = atom(0) local c = computed(function() return a() - b() end) @@ -120,16 +120,16 @@ test("should trigger inner effects in sequence", function() table.clear(order) startBatch() - setB(1) - setA(1) + b(1) + a(1) endBatch() assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`) end) test("should trigger inner effects in sequence in effect scope", function() - local a, setA = signal(0) - local b, setB = signal(0) + local a = atom(0) + local b = atom(0) local order: { string } = {} effectScope(function() @@ -148,8 +148,8 @@ test("should trigger inner effects in sequence in effect scope", function() table.clear(order) startBatch() - setB(1) - setA(1) + b(1) + a(1) endBatch() assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`) @@ -168,13 +168,13 @@ test("should custom effect support batch", function() end local logs: { string } = {} - local a = signal(0) - local b, setB = signal(0) + local a = atom(0) + local b = atom(0) local aa = computed(function() table.insert(logs, "aa-0") if a() == 0 then - setB(1) + b(1) end table.insert(logs, "aa-1") return nil @@ -196,8 +196,8 @@ test("should custom effect support batch", function() end) test("should duplicate subscribers do not affect the notify order", function() - local src1, setSrc1 = signal(0) - local src2, setSrc2 = signal(0) + local src1 = atom(0) + local src2 = atom(0) local order: { string } = {} effect(function() @@ -215,17 +215,17 @@ test("should duplicate subscribers do not affect the notify order", function() table.insert(order, "b") src1() end) - setSrc2(1) -- src1.subs: a -> b -> a + src2(1) -- src1.subs: a -> b -> a table.clear(order) - setSrc1(src1() + 1) + src1(src1() + 1) assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`) end) test("should handle side effect with inner effects", function() - local a, setA = signal(0) - local b, setB = signal(0) + local a = atom(0) + local b = atom(0) local order: { string } = {} effect(function() @@ -240,14 +240,14 @@ test("should handle side effect with inner effects", function() assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`) table.clear(order) - setB(1) - setA(1) + b(1) + a(1) assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`) end) end) test("should handle flags are indirectly updated during checkDirty", function() - local a, setA = signal(false) + local a = atom(false) local b = computed(function() return a() end) @@ -267,27 +267,27 @@ test("should handle flags are indirectly updated during checkDirty", function() triggers += 1 end) assert(triggers == 1, "did not run initially") - setA(true) + a(true) assert(triggers == 2, "did not update indirectly") end) test("should run cleanups in sequence", function() local order = {} - local a, setA = signal(false) + local a = atom(false) local stopEffect = effect(function() a() - onEffectCleanup(function() + onCleanup(function() table.insert(order, "a") end) - onEffectCleanup(function() + onCleanup(function() table.insert(order, "b") end) end) assert(table.concat(order) == "", "should not cleanup immediately") - setA(true) + a(true) assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`) table.clear(order) @@ -295,45 +295,45 @@ test("should run cleanups in sequence", function() assert(table.concat(order) == "ab", `bad dispose: {table.concat(order)}`) table.clear(order) - setA(false) + a(false) assert(table.concat(order) == "", "should not cleanup after update") end) test("should run inner effect cleanup", function() local cleanups = 0 - local src, setSrc = signal(false) + local src = atom(false) local stopEffect = effect(function() src() effect(function() - onEffectCleanup(function() + onCleanup(function() cleanups += 1 end) end) effect(function() - onEffectCleanup(function() + onCleanup(function() cleanups += 1 end) end) end) assert(cleanups == 0, "should not cleanup immediately") - setSrc(true) + src(true) assert(cleanups == 2, "should cleanup after outer update") stopEffect() assert(cleanups == 4, "should cleanup after outer dispose") - setSrc(false) + src(false) assert(cleanups == 4, "should not cleanup after update") end) test("should run cleanups outside any evaluation context", function() local calls = 0 - local a, setA = signal(0) - local b, setB = signal(0) + local a = atom(0) + local b = atom(0) local c = computed(function() if a() == 0 then effect(function() - onEffectCleanup(function() + onCleanup(function() b() end) end) @@ -348,11 +348,11 @@ test("should run cleanups outside any evaluation context", function() assert(calls == 1, "should run once") calls = 0 - setA(1) + a(1) assert(calls == 1, "should run once after a update") calls = 0 - setB(1) + b(1) assert(calls == 0, "should not run after b update") end) diff --git a/packages/charm/tests/signals/effectScope.spec.luau b/packages/charm/src/__tests__/core/effectScope.spec.luau similarity index 73% rename from packages/charm/tests/signals/effectScope.spec.luau rename to packages/charm/src/__tests__/core/effectScope.spec.luau index e48dcbe..c5dcbe8 100644 --- a/packages/charm/tests/signals/effectScope.spec.luau +++ b/packages/charm/src/__tests__/core/effectScope.spec.luau @@ -1,15 +1,14 @@ -local signals = require("../../src/signals") -local suite = require("@test/suite") +local core = require("../../core") +local suite = require("../../../../../test/suite") -local effect = signals.effect -local effectScope = signals.effectScope -local signal = signals.signal -local onEffectCleanup = signals.onEffectCleanup -local onScopeDispose = signals.onScopeDispose +local effect = core.effect +local effectScope = core.effectScope +local atom = core.atom +local onCleanup = core.onCleanup local test = suite.test test("should not trigger after stop", function() - local count, setCount = signal(1) + local count = atom(1) local triggers = 0 @@ -20,14 +19,14 @@ test("should not trigger after stop", function() end) assert(triggers == 1, "did not run initially") - setCount(2) + count(2) assert(triggers == 2, "did not run after update") end) - setCount(3) + count(3) assert(triggers == 3, "did not run after update outside scope") stopScope() - setCount(4) + count(4) assert(triggers == 3, "ran after stop") end) @@ -37,7 +36,7 @@ test("should run indirect scope cleanup", function() local cleanup local stopScope = effectScope(function() cleanup = effectScope(function() - onScopeDispose(function() + onCleanup(function() cleanups += 1 end) end) @@ -53,22 +52,22 @@ end) test("should run indirect effect cleanup", function() local cleanups = 0 - local a, setA = signal(false) + local a = atom(false) local stopScope = effectScope(function() effect(function() a() - onEffectCleanup(function() + onCleanup(function() cleanups += 1 end) end) end) assert(cleanups == 0, "should not cleanup immediately") - setA(true) + a(true) assert(cleanups == 1, "should cleanup once") stopScope() assert(cleanups == 2, "should cleanup after outer dispose") - setA(false) + a(false) assert(cleanups == 2, "should not cleanup after update") end) diff --git a/packages/charm/tests/signals/gc.spec.luau b/packages/charm/src/__tests__/core/gc.spec.luau similarity index 70% rename from packages/charm/tests/signals/gc.spec.luau rename to packages/charm/src/__tests__/core/gc.spec.luau index 2179894..9914415 100644 --- a/packages/charm/tests/signals/gc.spec.luau +++ b/packages/charm/src/__tests__/core/gc.spec.luau @@ -1,10 +1,10 @@ -local signals = require("../../src/signals") -local suite = require("@test/suite") +local core = require("../../core") +local suite = require("../../../../../test/suite") -local signal = signals.signal -local computed = signals.computed -local effect = signals.effect -local effectScope = signals.effectScope +local atom = core.atom +local computed = core.computed +local effect = core.effect +local effectScope = core.effectScope local test = suite.test --- Creates a weak reference to a value. @@ -23,13 +23,13 @@ local function gc() end end ---- In alien-signals, a computed value unlinks from dependencies when its last +--- In alien-core, a computed value unlinks from dependencies when its last --- parent scope is disposed. Calling a computed value outside of a scope links --- it to dependencies, but the dependencies hold strong references to the --- computed value, so it won't GC until called in a scope and disposed. --- --- This function releases a computed value by linking it to a scope and then ---- immediately closing it, causing alien-signals to unlink the computed value +--- immediately closing it, causing alien-core to unlink the computed value --- and allow it to be garbage collected. local function release(getter: () -> ()) effectScope(function() @@ -39,23 +39,23 @@ end test("should release computed cache", function() local srcRef = weakRef({}) - local src, setSrc = signal(srcRef()) + local src = atom(srcRef()) local c = computed(function() return src() end) c() -- cache src value - setSrc(nil) -- release value + src(nil) -- release value c = release(c) -- release computed gc() assert(srcRef() == nil, "should release computed cache") end) -test("should not release effect for signal", function() +test("should not release effect for atom", function() local effectCalls = 0 - local src, setSrc = signal(0) + local src = atom(0) effect(function() effectCalls += 1 @@ -65,7 +65,7 @@ test("should not release effect for signal", function() assert(effectCalls == 1, "should call effect once") gc() - setSrc(src() + 1) + src(src() + 1) assert(effectCalls == 2, "should call effect again") end) diff --git a/packages/charm/tests/signals/issue_48.spec.luau b/packages/charm/src/__tests__/core/issue_48.spec.luau similarity index 87% rename from packages/charm/tests/signals/issue_48.spec.luau rename to packages/charm/src/__tests__/core/issue_48.spec.luau index a60b79d..ba67e4e 100644 --- a/packages/charm/tests/signals/issue_48.spec.luau +++ b/packages/charm/src/__tests__/core/issue_48.spec.luau @@ -1,10 +1,10 @@ -local signals = require("../../src/signals") -local suite = require("@test/suite") +local core = require("../../core") +local suite = require("../../../../../test/suite") -local computed = signals.computed -local effect = signals.effect -local setCurrentSub = signals.setCurrentSub -local signal = signals.signal +local computed = core.computed +local effect = core.effect +local setCurrentSub = core.setCurrentSub +local atom = core.atom local test = suite.test type ReactionOptions = { @@ -90,7 +90,7 @@ local function reaction( end test("#48", function() - local source, setSource = signal(0) + local source = atom(0) local disposeInner: () -> () reaction(function() @@ -105,9 +105,9 @@ test("#48", function() end end) - setSource(1) - setSource(2) - setSource(3) + source(1) + source(2) + source(3) end) return {} diff --git a/packages/charm/tests/signals/topology.spec.luau b/packages/charm/src/__tests__/core/topology.spec.luau similarity index 89% rename from packages/charm/tests/signals/topology.spec.luau rename to packages/charm/src/__tests__/core/topology.spec.luau index 7fa961d..d3cdbf2 100644 --- a/packages/charm/tests/signals/topology.spec.luau +++ b/packages/charm/src/__tests__/core/topology.spec.luau @@ -1,15 +1,15 @@ -local signals = require("../../src/signals") -local suite = require("@test/suite") +local core = require("../../core") +local suite = require("../../../../../test/suite") -local computed = signals.computed -local effect = signals.effect -local signal = signals.signal +local computed = core.computed +local effect = core.effect +local atom = core.atom local describe = suite.describe local test = suite.test --[[ - Tests adopted with thanks from preact-signals implementation at - https://github.com/preactjs/signals/blob/main/packages/core/test/signal.test.tsx + Tests adopted with thanks from preact-core implementation at + https://github.com/preactjs/core/blob/main/packages/core/test/atom.test.tsx The MIT License (MIT) @@ -43,7 +43,7 @@ describe("graph updates", function() -- C -- | -- D - local a, setA = signal(2) + local a = atom(2) local b = computed(function() return a() - 1 @@ -64,12 +64,12 @@ describe("graph updates", function() assert(computeCalls == 1, "compute should be called once") computeCalls = 0 - setA(4) + a(4) d() assert(computeCalls == 1, "compute should drop update") end) - test("should only update every signal once (diamond graph)", function() + test("should only update every atom once (diamond graph)", function() -- In this scenario "D" should only update once when "A" receives -- an update. This is sometimes referred to as the "diamond" scenario. -- A @@ -78,7 +78,7 @@ describe("graph updates", function() -- \ / -- D - local a, setA = signal("a") + local a = atom("a") local b = computed(function() return a() end) @@ -96,12 +96,12 @@ describe("graph updates", function() assert(d() == "a a", "d should be 'a a'") assert(spyCalls == 1, "compute should be called once") - setA("aa") + a("aa") assert(d() == "aa aa", "d should be 'aa aa'") assert(spyCalls == 2, "compute should be called only twice") end) - test("should only update every signal once (diamond graph + tail)", function() + test("should only update every atom once (diamond graph + tail)", function() -- "E" will be likely updated twice if our mark+sweep logic is buggy. -- A -- / \ @@ -111,7 +111,7 @@ describe("graph updates", function() -- | -- E - local a, setA = signal("a") + local a = atom("a") local b = computed(function() return a() end) @@ -133,7 +133,7 @@ describe("graph updates", function() assert(e() == "a a", "e should be 'a a'") assert(spyCalls == 1, "compute should be called once") - setA("aa") + a("aa") assert(e() == "aa aa", "e should be 'aa aa'") assert(spyCalls == 2, "compute should be called only twice") end) @@ -141,7 +141,7 @@ describe("graph updates", function() test("should bail out if result is the same", function() -- Bail out if value of "B" never changes -- A->B->C - local a, setA = signal("a") + local a = atom("a") local b = computed(function() a() return "foo" @@ -157,12 +157,12 @@ describe("graph updates", function() assert(c() == "foo", "c should be 'foo'") assert(spyCalls == 1, "compute should be called once") - setA("aa") + a("aa") assert(c() == "foo", "c should be 'foo'") assert(spyCalls == 1, "compute should bail out") end) - test("should only update every signal once (jagged diamond graph + tails)", function() + test("should only update every atom once (jagged diamond graph + tails)", function() -- "F" and "G" will be likely updated twice if our mark+sweep logic is buggy. -- A -- / \ @@ -173,7 +173,7 @@ describe("graph updates", function() -- E -- / \ -- F G - local a, setA = signal("a") + local a = atom("a") local b = computed(function() return a() @@ -222,7 +222,7 @@ describe("graph updates", function() fSpyCalls, fSpyTime = 0, 0 gSpyCalls, gSpyTime = 0, 0 - setA("b") + a("b") assert(e() == "b b", "e should be 'b b'") assert(eSpyCalls == 1, "e should run once after update") @@ -237,7 +237,7 @@ describe("graph updates", function() fSpyCalls, fSpyTime = 0, 0 gSpyCalls, gSpyTime = 0, 0 - setA("c") + a("c") assert(e() == "c c", "e should be 'c c'") assert(eSpyCalls == 1, "e should run once after update") @@ -254,11 +254,11 @@ describe("graph updates", function() assert(eSpyTime < gSpyTime, "e should be called before g") end) - test("should only subscribe to signals listened to", function() + test("should only subscribe to core listened to", function() -- *A -- / \ -- *B C <- we don't listen to C - local a, setA = signal("a") + local a = atom("a") local b = computed(function() return a() @@ -273,12 +273,12 @@ describe("graph updates", function() assert(b() == "a", "b should be 'a'") assert(spyCalls == 0, "compute should not run") - setA("aa") + a("aa") assert(b() == "aa", "b should be 'aa'") assert(spyCalls == 0, "compute should not run") end) - test("should only subscribe to signals listened to II", function() + test("should only subscribe to core listened to II", function() -- Here both "B" and "C" are active in the beginning, but -- "B" becomes inactive later. At that point it should -- not receive any updates anymore. @@ -287,7 +287,7 @@ describe("graph updates", function() -- *B D <- we don't listen to C -- | -- *C - local a, setA = signal("a") + local a = atom("a") local spyBCalls = 0 local spyB = function() spyBCalls += 1 @@ -318,7 +318,7 @@ describe("graph updates", function() spyCCalls = 0 unsub() - setA("aa") + a("aa") assert(spyBCalls == 0, "b should not run") assert(spyCCalls == 0, "c should not run") @@ -335,7 +335,7 @@ describe("graph updates", function() -- B *C <- returns same value every time -- \ / -- D - local a, setA = signal("a") + local a = atom("a") local b = computed(function() return a() end) @@ -353,7 +353,7 @@ describe("graph updates", function() assert(d() == "a c", "d should be 'a c'") spyResult = nil - setA("aa") + a("aa") d() assert(spyResult == "aa c", "d should have returned 'aa c'") end) @@ -367,7 +367,7 @@ describe("graph updates", function() -- B *C *D -- \ | / -- E - local a, setA = signal("a") + local a = atom("a") local b = computed(function() return a() end) @@ -389,13 +389,13 @@ describe("graph updates", function() assert(e() == "a c d", "e should be 'a c d'") spyResult = nil - setA("aa") + a("aa") e() assert(spyResult == "aa c d", "e should have returned 'aa c d'") end) test("should support lazy branches", function() - local a, setA = signal(0) + local a = atom(0) local b = computed(function() return a() end) @@ -404,10 +404,10 @@ describe("graph updates", function() end) assert(c() == 0, "c should be 0") - setA(1) + a(1) assert(c() == 1, "c should be 1") - setA(0) + a(0) assert(c() == 0, "c should be 0 again") end) @@ -419,7 +419,7 @@ describe("graph updates", function() -- *B *C -- \ / -- D - local a, setA = signal("a") + local a = atom("a") local b = computed(function() a() return "b" @@ -438,14 +438,14 @@ describe("graph updates", function() assert(d() == "b c", "d should be 'b c'") spyCalls = 0 - setA("aa") + a("aa") assert(spyCalls == 0, "d should not run") end) end) describe("error handling", function() test("should keep graph consistent on errors during activation", function() - local a, setA = signal(0) + local a = atom(0) local b = computed(function() error("fail") end) @@ -459,12 +459,12 @@ describe("error handling", function() assert(not success, "should throw") assert(string.match(result, "fail"), "should throw fail") - setA(1) + a(1) assert(c() == 1, "c should recover and return 1") end) test("should keep graph consistent on errors in computeds", function() - local a, setA = signal(0) + local a = atom(0) local b = computed(function() if a() == 1 then error("fail") @@ -477,14 +477,14 @@ describe("error handling", function() assert(c() == 0, "c should be 0") - setA(1) + a(1) local success, result = pcall(function() b() end) assert(not success, "should throw") assert(string.match(result, "fail"), "should throw fail") - setA(2) + a(2) assert(c() == 2, "c should recover and return 2") end) end) diff --git a/packages/charm/tests/signals/untrack.spec.luau b/packages/charm/src/__tests__/core/untrack.spec.luau similarity index 57% rename from packages/charm/tests/signals/untrack.spec.luau rename to packages/charm/src/__tests__/core/untrack.spec.luau index f8f738f..177c3d7 100644 --- a/packages/charm/tests/signals/untrack.spec.luau +++ b/packages/charm/src/__tests__/core/untrack.spec.luau @@ -1,13 +1,13 @@ -local signals = require("../../src/signals") -local suite = require("@test/suite") +local core = require("../../core") +local suite = require("../../../../../test/suite") -local computed = signals.computed -local setCurrentSub = signals.setCurrentSub -local signal = signals.signal +local computed = core.computed +local setCurrentSub = core.setCurrentSub +local atom = core.atom local test = suite.test test("should pause tracking", function() - local src, setSrc = signal(0) + local src = atom(0) local c = computed(function() local currentSub = setCurrentSub(nil) local value = src() @@ -16,7 +16,7 @@ test("should pause tracking", function() end) assert(c() == 0, "should not track src") - setSrc(1) + src(1) assert(c() == 0, "should not track src") end) diff --git a/packages/charm/tests/listen.spec.luau b/packages/charm/src/__tests__/listen.spec.luau similarity index 96% rename from packages/charm/tests/listen.spec.luau rename to packages/charm/src/__tests__/listen.spec.luau index 1142347..097d2aa 100644 --- a/packages/charm/tests/listen.spec.luau +++ b/packages/charm/src/__tests__/listen.spec.luau @@ -1,5 +1,5 @@ -local Charm = require("../src") -local suite = require("@test/suite") +local Charm = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local listen = Charm.listen diff --git a/packages/charm/tests/mapped.spec.luau b/packages/charm/src/__tests__/mapped.spec.luau similarity index 97% rename from packages/charm/tests/mapped.spec.luau rename to packages/charm/src/__tests__/mapped.spec.luau index 3d0fcf5..2d1e857 100644 --- a/packages/charm/tests/mapped.spec.luau +++ b/packages/charm/src/__tests__/mapped.spec.luau @@ -1,5 +1,5 @@ -local Charm = require("../src") -local suite = require("@test/suite") +local Charm = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local mapped = Charm.mapped diff --git a/packages/charm/tests/observe.spec.luau b/packages/charm/src/__tests__/observe.spec.luau similarity index 94% rename from packages/charm/tests/observe.spec.luau rename to packages/charm/src/__tests__/observe.spec.luau index 84f5e77..ed392f4 100644 --- a/packages/charm/tests/observe.spec.luau +++ b/packages/charm/src/__tests__/observe.spec.luau @@ -1,5 +1,5 @@ -local Charm = require("../src") -local suite = require("@test/suite") +local Charm = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local computed = Charm.computed diff --git a/packages/charm/src/__tests__/spec.luau b/packages/charm/src/__tests__/spec.luau new file mode 100644 index 0000000..7d0d54a --- /dev/null +++ b/packages/charm/src/__tests__/spec.luau @@ -0,0 +1,45 @@ +local suite = require("../../../../test/suite") + +suite.describe("core", function() + suite.describe("computed", function() + require("./core/computed.spec") + end) + suite.describe("effect", function() + require("./core/effect.spec") + end) + suite.describe("effectScope", function() + require("./core/effectScope.spec") + end) + suite.describe("issue_48", function() + require("./core/issue_48.spec") + end) + suite.describe("gc", function() + require("./core/gc.spec") + end) + suite.describe("topology", function() + require("./core/topology.spec") + end) + suite.describe("untrack", function() + require("./core/untrack.spec") + end) +end) + +suite.describe("charm", function() + suite.describe("listen", function() + require("./listen.spec") + end) + suite.describe("mapped", function() + require("./mapped.spec") + end) + suite.describe("observe", function() + require("./observe.spec") + end) + suite.describe("subscribe", function() + require("./subscribe.spec") + end) + suite.describe("untracked", function() + require("./untracked.spec") + end) +end) + +return {} diff --git a/packages/charm/tests/subscribe.spec.luau b/packages/charm/src/__tests__/subscribe.spec.luau similarity index 97% rename from packages/charm/tests/subscribe.spec.luau rename to packages/charm/src/__tests__/subscribe.spec.luau index 67bebda..d91a952 100644 --- a/packages/charm/tests/subscribe.spec.luau +++ b/packages/charm/src/__tests__/subscribe.spec.luau @@ -1,5 +1,5 @@ -local Charm = require("../src") -local suite = require("@test/suite") +local Charm = require("../") +local suite = require("../../../../test/suite") type Atom = Charm.Atom diff --git a/packages/charm/tests/untracked.spec.luau b/packages/charm/src/__tests__/untracked.spec.luau similarity index 95% rename from packages/charm/tests/untracked.spec.luau rename to packages/charm/src/__tests__/untracked.spec.luau index 53f7767..37a40aa 100644 --- a/packages/charm/tests/untracked.spec.luau +++ b/packages/charm/src/__tests__/untracked.spec.luau @@ -1,5 +1,5 @@ -local Charm = require("../src") -local suite = require("@test/suite") +local Charm = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local computed = Charm.computed diff --git a/packages/charm/src/atom.luau b/packages/charm/src/atom.luau deleted file mode 100644 index e4651ac..0000000 --- a/packages/charm/src/atom.luau +++ /dev/null @@ -1,48 +0,0 @@ -local signals = require("./signals") -local types = require("./types") - -type Atom = types.Atom -type Getter = types.Getter -type Setter = types.Setter -type Update = types.Update - -local signal = signals.signal - -type Equals = (current: T, incoming: T) -> boolean - -local function createAtom(getter: Getter, setter: Setter): Atom - local function atom(...: Update): T - return if select("#", ...) == 0 then getter() else setter((...)) - end - - return atom -end - -local function createAtomWithEqualityCheck(getter: Getter, setter: Setter, equals: Equals): Atom - local function atom(...: Update): T - if select("#", ...) == 0 then - return getter() - end - - local nextState = (...) - - return setter(function(currentState): T - local nextState = if type(nextState) == "function" then nextState(currentState) else nextState - return if equals(currentState, nextState) then currentState else nextState - end) - end - - return atom -end - -local function atom(state: T, equals: Equals?): Atom - local getter, setter = signal(state) - - if not equals then - return createAtom(getter, setter) - else - return createAtomWithEqualityCheck(getter, setter, equals) - end -end - -return atom diff --git a/packages/charm/src/batched.luau b/packages/charm/src/batched.luau index 31be1dc..685365a 100644 --- a/packages/charm/src/batched.luau +++ b/packages/charm/src/batched.luau @@ -1,16 +1,14 @@ -local signals = require("./signals") +local core = require("./core") -type ReactiveNode = signals.ReactiveNode - -local startBatch = signals.startBatch -local endBatch = signals.endBatch +local startBatch = core.startBatch +local endBatch = core.endBatch local function handleBatched(ok: boolean, ...) endBatch() if ok then return ... else - error(...) + error((...)) end end diff --git a/packages/charm/src/signals.luau b/packages/charm/src/core.luau similarity index 52% rename from packages/charm/src/signals.luau rename to packages/charm/src/core.luau index a1f4ae0..75c8319 100644 --- a/packages/charm/src/signals.luau +++ b/packages/charm/src/core.luau @@ -1,5 +1,5 @@ --[[ - alien-signals 2.0.1 implementation adopted from + alien-signals 2.0.5 implementation adopted from https://github.com/Nicell/alien-signals-luau https://github.com/stackblitz/alien-signals @@ -26,8 +26,9 @@ SOFTWARE. ]] -local noYield = require("./noYield") +local strict = require("./strict") local system = require("./system") +local utils = require("./utils") export type ReactiveFlags = system.ReactiveFlags export type ReactiveNode = system.ReactiveNode @@ -35,6 +36,7 @@ export type Link = system.Link local ReactiveFlags = system.ReactiveFlags local createReactiveSystem = system.createReactiveSystem +local noYield = utils.noYield local NONE = 0b0000000 local MUTABLE = 0b0000001 @@ -48,23 +50,22 @@ type EffectScope = ReactiveNode & { } type Effect = ReactiveNode & { - fn: () -> (), + fn: () -> ...(() -> ())?, cleanups: { () -> () }, } type Computed = ReactiveNode & { value: T?, getter: (previousValue: T?) -> T, + equals: ((T, T) -> boolean)?, } -type Signal = ReactiveNode & { +type Atom = ReactiveNode & { previousValue: T, value: T, + equals: ((T, T) -> boolean)?, } -local pauseStack: { ReactiveNode? } = {} -local queuedEffects: { Effect | EffectScope? } = {} - local link: (dep: ReactiveNode, sub: ReactiveNode) -> () local unlink: (link: Link, sub: ReactiveNode?) -> Link? local propagate: (link: Link) -> () @@ -74,9 +75,10 @@ local startTracking: (sub: ReactiveNode) -> () local shallowPropagate: (link: Link) -> () local batchDepth = 0 - -local notifyIndex = 0 +local queuedEffects: { Effect | EffectScope? } = {} local queuedEffectsLength = 0 +local notifyIndex = 0 +local activeSubTracking = true local activeSub: ReactiveNode? local activeScope: Effect | EffectScope? @@ -100,50 +102,58 @@ local function setCurrentScope(scope: Effect | EffectScope?) return prevScope end -local function onEffectCleanup(cleanup: () -> ()) +local function setCurrentTracking(tracking: boolean): boolean + local prevTracking = activeSubTracking + activeSubTracking = tracking + return prevTracking +end + +local function runCleanups(effect: EffectScope) + for _, cleanup in effect.cleanups do + local prev = setCurrentSub(nil) + local success, result: unknown = (if strict.enabled then noYield else pcall)(cleanup) + setCurrentSub(prev) + if not success then + error(result) + end + end + table.clear(effect.cleanups) +end + +local function onCleanup(cleanup: () -> (), failSilently: boolean?) if activeSub then table.insert((activeSub :: Effect).cleanups, cleanup) - end -end - -local function onScopeDispose(cleanup: () -> ()) - if activeScope then + elseif activeScope then table.insert(activeScope.cleanups, cleanup) + elseif not failSilently then + warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2)) end end -local function cleanupEffect(e: Effect | EffectScope) - for _, cleanup in e.cleanups :: { () -> () } do - local prevSub = setCurrentSub(nil) - local success, result: unknown = noYield(cleanup) - setCurrentSub(prevSub) - if not success then - error(result) - end - end - table.clear(e.cleanups) -end - -local function run(e: Effect | EffectScope, flags: ReactiveFlags): () - if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then - cleanupEffect(e) - local prev = setCurrentSub(e) - startTracking(e) - local success, result: unknown = noYield((e :: Effect).fn) +local function run(effect: Effect | EffectScope, flags: ReactiveFlags): () + if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then + runCleanups(effect) + local prev = setCurrentSub(effect) + local prevTracking = setCurrentTracking(true) + startTracking(effect) + local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)((effect :: Effect).fn) setCurrentSub(prev) - endTracking(e) + endTracking(effect) + setCurrentTracking(prevTracking) if not success then error(result) + elseif result then + table.insert(effect.cleanups, result) end return - elseif bit32.band(flags, PENDING) ~= 0 then - (e :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING)) + elseif bit32.btest(flags, PENDING) then + (effect :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING)) end - local link = e.deps + local link = effect.deps while link do local dep = link.dep local depFlags = dep.flags - if bit32.band(depFlags, QUEUED) ~= 0 then + if bit32.btest(depFlags, QUEUED) then depFlags = bit32.band(depFlags, bit32.bnot(QUEUED)) dep.flags = depFlags run(dep :: Effect, depFlags) @@ -179,209 +189,154 @@ local function endBatch() end end ---- @deprecated Will be removed in the next major version. Use ---- `const pausedSub = setCurrentSub(undefined)` instead for better performance. -local function pauseTracking() - table.insert(pauseStack, setCurrentSub(nil)) -end - ---- @deprecated Will be removed in the next major version. Use ---- `setCurrentSub(pausedSub)` instead for better performance. -local function resumeTracking() - setCurrentSub(table.remove(pauseStack)) -end - -local function updateComputed(c: Computed): boolean - local prevSub = setCurrentSub(c) - startTracking(c) - local oldValue = c.value - local success, result = noYield(c.getter, oldValue) +local function updateComputed(computed: Computed): boolean + local prevSub = setCurrentSub(computed) + local prevTracking = setCurrentTracking(true) + startTracking(computed) + local oldValue = computed.value + local success, result = (if strict.enabled then noYield else pcall)(computed.getter, oldValue) setCurrentSub(prevSub) - endTracking(c) + endTracking(computed) + setCurrentTracking(prevTracking) if not success then error(result) end - if oldValue ~= result then - c.value = result + local equals = computed.equals + if if equals then not equals(oldValue, result) else oldValue ~= result then + computed.value = result return true else return false end end -local function updateSignal(s: Signal, value: any): boolean - s.flags = MUTABLE - if s.previousValue ~= value then - s.previousValue = value +local function updateAtom(atom: Atom, value: any): boolean + atom.flags = MUTABLE + if atom.previousValue ~= value then + atom.previousValue = value return true else return false end end -local function signalGetterSetter(this: Signal): (() -> T, (value: ((T) -> T) | T) -> T) - local function getter(): T - local value = this.value - if bit32.band(this.flags, DIRTY) ~= 0 then - if updateSignal(this, value) then - local subs = this.subs - if subs then - shallowPropagate(subs) - end +local function readAtom(atom: Atom): T + local value = atom.value + if bit32.btest(atom.flags, DIRTY) then + if updateAtom(atom, value) then + local subs = atom.subs + if subs then + shallowPropagate(subs) end end - if activeSub then - link(this, activeSub) - end - return value end + if activeSub and activeSubTracking then + link(atom, activeSub) + end + return value +end - local function setter(newValue: ((T) -> T) | T): T - local value = this.value - local newValue = newValue :: T +local function writeAtom(atom: Atom, oldValue: T, newValue: T): T + local equals = atom.equals + if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then + atom.value = newValue + atom.flags = bit32.bor(MUTABLE, DIRTY) + local subs = atom.subs + if subs then + propagate(subs) + if batchDepth == 0 then + flush() + end + end + return newValue + end + return oldValue +end + +local function atomGetterSetter(atom: Atom): (...((T) -> T) | T) -> T + return function(...) + if select("#", ...) == 0 then + return readAtom(atom) + end + local oldValue = atom.value + local newValue = ... if type(newValue) == "function" then local prev = setCurrentSub(nil) - local success, result = noYield(newValue, value) + local success, result = (if strict.enabled then noYield else pcall)(newValue, oldValue) setCurrentSub(prev) if not success then error(result) end newValue = result end - if value ~= newValue then - this.value = newValue - this.flags = bit32.bor(MUTABLE, DIRTY) - local subs = this.subs - if subs then - propagate(subs) - if batchDepth == 0 then - flush() - end - end - end - return newValue + return writeAtom(atom, oldValue, newValue :: T) end - - return getter, setter end -local function computedGetter(this: Computed): () -> T +local function computedGetter(computed: Computed): () -> T return function() - local flags = this.flags + local flags = computed.flags if - bit32.band(flags, DIRTY) ~= 0 - or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this)) + bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed)) then - if updateComputed(this) then - local subs = this.subs + if updateComputed(computed) then + local subs = computed.subs if subs then shallowPropagate(subs) end end - elseif bit32.band(flags, PENDING) ~= 0 then - this.flags = bit32.band(flags, bit32.bnot(PENDING)) + elseif bit32.btest(flags, PENDING) then + computed.flags = bit32.band(flags, bit32.bnot(PENDING)) end if activeSub then - link(this, activeSub) + if activeSubTracking then + link(computed, activeSub) + end elseif activeScope then - link(this, activeScope) + link(computed, activeScope) end - return this.value :: T + return computed.value :: T end end -local function effectStop(this: Effect | EffectScope): () -> () +local function effectStop(self: Effect | EffectScope): () -> () return function() - local dep = this.deps + local dep = self.deps while dep do - dep = unlink(dep, this) + dep = unlink(dep, self) end - local sub = this.subs + local sub = self.subs if sub then unlink(sub) end - (this :: Effect).flags = NONE - cleanupEffect(this) + (self :: Effect).flags = NONE + runCleanups(self) end end -local function signal(initialValue: T): (() -> T, (value: ((T) -> T) | T) -> T) - return signalGetterSetter({ - previousValue = initialValue, - value = initialValue, - flags = MUTABLE, - }) -end - -local function computed(getter: (previousValue: T?) -> T): () -> T - return computedGetter({ - flags = bit32.bor(MUTABLE, DIRTY), - getter = getter, - }) -end - -local function effect(fn: () -> ()): () -> () - local e: Effect = { - fn = fn, - cleanups = {}, - flags = WATCHING, - } - if activeSub then - link(e, activeSub) - elseif activeScope then - link(e, activeScope) - end - local prev = setCurrentSub(e) - local success, result: unknown = noYield(fn) - setCurrentSub(prev) - if not success then - error(result) - end - return effectStop(e) -end - -local function effectScope(fn: () -> (), detached: boolean?): () -> () - local e: EffectScope = { - cleanups = {}, - flags = NONE, - } - if activeScope and not detached then - link(e, activeScope) - end - local prevSub = setCurrentSub(nil) - local prevScope = setCurrentScope(e) - local success, result: unknown = noYield(fn) - setCurrentScope(prevScope) - setCurrentSub(prevSub) - if not success then - error(result) - end - return effectStop(e) -end - -local function update(signal: Signal | Computed): boolean - if (signal :: Computed).getter then - return updateComputed(signal :: Computed) +local function update(atom: Atom | Computed): boolean + if (atom :: Computed).getter then + return updateComputed(atom :: Computed) else - return updateSignal(signal :: Signal, signal.value) + return updateAtom(atom :: Atom, atom.value) end end -local function notify(e: Effect | EffectScope) - local flags = e.flags - if bit32.band(flags, QUEUED) == 0 then - (e :: Effect).flags = bit32.bor(flags, QUEUED) - local subs = e.subs +local function notify(effect: Effect | EffectScope) + local flags = effect.flags + if not bit32.btest(flags, QUEUED) then + (effect :: Effect).flags = bit32.bor(flags, QUEUED) + local subs = effect.subs if subs then notify(subs.sub :: Effect) else queuedEffectsLength += 1 - queuedEffects[queuedEffectsLength] = e + queuedEffects[queuedEffectsLength] = effect end end end -local function unwatched(node: Signal | Computed | Effect | EffectScope) +local function unwatched(node: Atom | Computed | Effect | EffectScope) if (node :: Computed).getter then local toRemove = node.deps if toRemove then @@ -390,11 +345,73 @@ local function unwatched(node: Signal | Computed | Effect | EffectScope) toRemove = unlink(toRemove, node) until not toRemove end - elseif (node :: Effect).cleanups ~= nil then + elseif (node :: Effect).cleanups then effectStop(node :: Effect)() end end +local function atom(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T + return atomGetterSetter({ + previousValue = initialValue, + value = initialValue, + flags = MUTABLE, + equals = equals, + }) +end + +local function computed(getter: (previousValue: T?) -> T, equals: ((T, T) -> boolean)?): () -> T + return computedGetter({ + flags = bit32.bor(MUTABLE, DIRTY), + getter = getter, + equals = equals, + }) +end + +local function effect(fn: () -> ...(() -> ())?): () -> () + local self: Effect = { + fn = fn, + flags = WATCHING, + cleanups = {}, + } + if activeSub then + link(self, activeSub) + elseif activeScope then + link(self, activeScope) + end + local prev = setCurrentSub(self) + local prevTracking = setCurrentTracking(true) + local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn) + setCurrentSub(prev) + setCurrentTracking(prevTracking) + if not success then + error(result) + elseif result then + table.insert(self.cleanups, result) + end + return effectStop(self) +end + +local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> () + local self: EffectScope = { + flags = NONE, + cleanups = {}, + } + if activeScope and not detached then + link(self, activeScope) + end + local prevSub = setCurrentSub(nil) + local prevScope = setCurrentScope(self) + local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn) + setCurrentScope(prevScope) + setCurrentSub(prevSub) + if not success then + error(result) + elseif result then + table.insert(self.cleanups, result) + end + return effectStop(self) +end + local reactiveSystem = createReactiveSystem({ update = update :: (ReactiveNode) -> boolean, notify = notify :: (ReactiveNode) -> (), @@ -416,14 +433,12 @@ return { setCurrentSub = setCurrentSub, getCurrentScope = getCurrentScope, setCurrentScope = setCurrentScope, + setCurrentTracking = setCurrentTracking, + onCleanup = onCleanup, getBatchDepth = getBatchDepth, startBatch = startBatch, endBatch = endBatch, - pauseTracking = pauseTracking, - resumeTracking = resumeTracking, - onEffectCleanup = onEffectCleanup, - onScopeDispose = onScopeDispose, - signal = signal, + atom = atom, computed = computed, effect = effect, effectScope = effectScope, diff --git a/packages/charm/src/effect.luau b/packages/charm/src/effect.luau deleted file mode 100644 index 2eb0802..0000000 --- a/packages/charm/src/effect.luau +++ /dev/null @@ -1,26 +0,0 @@ -local signals = require("./signals") - -local onEffectCleanup = signals.onEffectCleanup - -local function effect(callback: (dispose: () -> ()) -> ...(() -> ())?): () -> () - local disposed = false - - local function dispose() - disposed = true - end - - dispose = signals.effect(function() - local cleanup = callback(dispose) - if cleanup then - onEffectCleanup(cleanup) - end - end) - - if disposed then - dispose() - end - - return dispose -end - -return effect diff --git a/packages/charm/src/effectScope.luau b/packages/charm/src/effectScope.luau deleted file mode 100644 index 7a40ee2..0000000 --- a/packages/charm/src/effectScope.luau +++ /dev/null @@ -1,14 +0,0 @@ -local signals = require("./signals") - -local onScopeDispose = signals.onScopeDispose - -local function effectScope(callback: () -> ...(() -> ())?, detached: boolean?): () -> () - return signals.effectScope(function() - local cleanup = callback() - if cleanup then - onScopeDispose(cleanup) - end - end, detached) -end - -return effectScope diff --git a/packages/charm/src/index.d.ts b/packages/charm/src/index.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/charm/src/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/charm/src/init.luau b/packages/charm/src/init.luau index 0f049ab..e583ae6 100644 --- a/packages/charm/src/init.luau +++ b/packages/charm/src/init.luau @@ -1,25 +1,25 @@ -local atom = require("@self/atom") local batched = require("@self/batched") -local effect = require("@self/effect") +local core = require("@self/core") local listen = require("@self/listen") local mapped = require("@self/mapped") local observe = require("@self/observe") -local onCleanup = require("@self/onCleanup") -local signals = require("@self/signals") +local strict = require("@self/strict") local subscribe = require("@self/subscribe") local types = require("@self/types") local untracked = require("@self/untracked") export type Atom = types.Atom -export type Getter = types.Getter export type Selector = types.Selector -export type Setter = types.Setter -export type Update = types.Update -local computed = signals.computed -local effectScope = signals.effectScope +local atom = core.atom +local computed = core.computed +local effect = core.effect +local effectScope = core.effectScope +local onCleanup = core.onCleanup return { + core = core, + strict = strict, atom = atom, batched = batched, computed = computed, @@ -28,8 +28,7 @@ return { listen = listen, mapped = mapped, observe = observe, + onCleanup = onCleanup, subscribe = subscribe, untracked = untracked, - onCleanup = onCleanup, - signals = signals, } diff --git a/packages/charm/src/listen.luau b/packages/charm/src/listen.luau index 89688cf..865e704 100644 --- a/packages/charm/src/listen.luau +++ b/packages/charm/src/listen.luau @@ -1,9 +1,8 @@ -local signals = require("./signals") +local core = require("./core") +local untracked = require("./untracked") -local computed = signals.computed -local effect = signals.effect -local effectScope = signals.effectScope -local onEffectCleanup = signals.onEffectCleanup +local computed = core.computed +local effect = core.effect local function listen(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> () local tracked = computed(function() @@ -20,12 +19,7 @@ local function listen(selector: () -> T, listener: (state: T, previous: T?, d dispose = effect(function() local previous = current current = tracked() - - local stopScope = effectScope(function() - listener(current, previous, dispose) - end) - - onEffectCleanup(stopScope) + untracked(listener, current, previous, dispose) end) if disposed then diff --git a/packages/charm/src/mapped.luau b/packages/charm/src/mapped.luau index 847968c..f6289be 100644 --- a/packages/charm/src/mapped.luau +++ b/packages/charm/src/mapped.luau @@ -1,35 +1,26 @@ -local noYield = require("./noYield") -local signals = require("./signals") +local core = require("./core") -local computed = signals.computed -local setCurrentSub = signals.setCurrentSub +local computed = core.computed +local setCurrentSub = core.setCurrentSub type Mapped = & ((selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO }) & ((selector: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO }) -local function ensureCanMutateOutput(output: { [KO]: VO }, previous: { [KO]: VO }?): { [KO]: VO } - return if output == previous then table.clone(output) else output -end - local function mapped(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO } local currentData: { [KI]: VI } = {} local keyCache: { [KI]: KO } = {} local function map(value: VI, key: KI): (VO, KO) local currentSub = setCurrentSub(nil) - local success, valueOut, keyOut = noYield(mapper, value, key) + local success, valueOut, keyOut = pcall(mapper, value, key) setCurrentSub(currentSub) if not success then error(`mapped() transform function failed: {valueOut}`) end - if keyOut == nil then - keyOut = key :: any - end - - return valueOut, keyOut + return valueOut, if keyOut == nil then key else keyOut :: any end return computed(function(previousOutput) @@ -41,11 +32,9 @@ local function mapped(selector: () -> { [KI]: VI }, mapper: (ite if currentData[key] ~= nil then continue end - local cachedKey = keyCache[key] - if cachedKey then - currentOutput = ensureCanMutateOutput(currentOutput, previousOutput) + currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput currentOutput[cachedKey] = nil keyCache[key] = nil end @@ -55,11 +44,9 @@ local function mapped(selector: () -> { [KI]: VI }, mapper: (ite if value == previousData[key] then continue end - local valueOut, keyOut = map(value, key) - if currentOutput[keyOut] ~= valueOut then - currentOutput = ensureCanMutateOutput(currentOutput, previousOutput) + currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput currentOutput[keyOut] = valueOut keyCache[key] = keyOut end diff --git a/packages/charm/src/observe.luau b/packages/charm/src/observe.luau index 9a0d1a9..7272584 100644 --- a/packages/charm/src/observe.luau +++ b/packages/charm/src/observe.luau @@ -1,9 +1,8 @@ -local onCleanup = require("./onCleanup") -local signals = require("./signals") +local core = require("./core") -local effect = signals.effect -local effectScope = signals.effectScope -local onScopeDispose = signals.onScopeDispose +local effect = core.effect +local effectScope = core.effectScope +local onCleanup = core.onCleanup local function observe(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> () local scopes: { [K]: () -> () } = {} @@ -11,8 +10,9 @@ local function observe(selector: () -> { [K]: V }, observer: (item: V, key local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V }) for key in previousItems do - if currentItems[key] == nil and scopes[key] then - scopes[key]() + local dispose = scopes[key] + if currentItems[key] == nil and dispose then + dispose() scopes[key] = nil end end @@ -20,10 +20,7 @@ local function observe(selector: () -> { [K]: V }, observer: (item: V, key for key, value in currentItems do if previousItems[key] == nil then scopes[key] = effectScope(function() - local cleanup = observer(value, key) - if cleanup then - onScopeDispose(cleanup) - end + return observer(value, key) end, true) end end @@ -39,7 +36,6 @@ local function observe(selector: () -> { [K]: V }, observer: (item: V, key local disposeEffect = effect(function() local previousItems = currentItems currentItems = selector() - if currentItems ~= previousItems then updateScopes(currentItems, previousItems) end diff --git a/packages/charm/src/onCleanup.luau b/packages/charm/src/onCleanup.luau deleted file mode 100644 index f96266d..0000000 --- a/packages/charm/src/onCleanup.luau +++ /dev/null @@ -1,20 +0,0 @@ -local signals = require("./signals") - -local onEffectCleanup = signals.onEffectCleanup -local onScopeDispose = signals.onScopeDispose -local getCurrentScope = signals.getCurrentScope -local getCurrentSub = signals.getCurrentSub - -local warn = warn or error :: never - -local function onCleanup(cleanup: () -> (), failSilently: boolean?) - if getCurrentSub() then - onEffectCleanup(cleanup) - elseif getCurrentScope() then - onScopeDispose(cleanup) - elseif _G.__DEV__ and not failSilently then - warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2)) - end -end - -return onCleanup diff --git a/packages/charm/src/strict.luau b/packages/charm/src/strict.luau new file mode 100644 index 0000000..1955aa1 --- /dev/null +++ b/packages/charm/src/strict.luau @@ -0,0 +1,8 @@ +local function inlineTest(): string + return debug.info(1, "n") +end + +return { + -- Enable strict mode in O2 + enabled = inlineTest() == "inlineTest", +} diff --git a/packages/charm/src/subscribe.luau b/packages/charm/src/subscribe.luau index 73c3782..1ceca66 100644 --- a/packages/charm/src/subscribe.luau +++ b/packages/charm/src/subscribe.luau @@ -1,9 +1,8 @@ -local signals = require("./signals") +local core = require("./core") +local untracked = require("./untracked") -local computed = signals.computed -local effect = signals.effect -local effectScope = signals.effectScope -local onEffectCleanup = signals.onEffectCleanup +local computed = core.computed +local effect = core.effect local function subscribe(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> () local tracked = computed(function() @@ -16,17 +15,11 @@ local function subscribe(selector: () -> T, listener: (state: T, lastState: T return effect(function() local previous = current current = tracked() - if mounted then mounted = false - return + else + untracked(listener, current, previous) end - - local stopScope = effectScope(function() - listener(current, previous) - end) - - onEffectCleanup(stopScope) end) end diff --git a/packages/charm/src/system.luau b/packages/charm/src/system.luau index 0c6334b..d3d5692 100644 --- a/packages/charm/src/system.luau +++ b/packages/charm/src/system.luau @@ -100,7 +100,7 @@ local function createReactiveSystem(props: { return end local nextDep: Link? - local recursedCheck = bit32.band(sub.flags, RECURSED_CHECK) ~= 0 + local recursedCheck = bit32.btest(sub.flags, RECURSED_CHECK) if recursedCheck then nextDep = if prevDep then prevDep.nextDep else sub.deps if nextDep and nextDep.dep == dep then @@ -178,14 +178,14 @@ local function createReactiveSystem(props: { local flags = sub.flags - if bit32.band(flags, bit32.bor(MUTABLE, WATCHING)) ~= 0 then - if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then + if bit32.btest(flags, bit32.bor(MUTABLE, WATCHING)) then + if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then sub.flags = bit32.bor(flags, PENDING) - elseif bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED)) == 0 then + elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then flags = NONE - elseif bit32.band(flags, RECURSED_CHECK) == 0 then + elseif not bit32.btest(flags, RECURSED_CHECK) then sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING) - elseif bit32.band(flags, bit32.bor(DIRTY, PENDING)) == 0 and isValidLink(link, sub) then + elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING)) flags = bit32.band(flags, MUTABLE) else @@ -193,11 +193,11 @@ local function createReactiveSystem(props: { flags = NONE end - if bit32.band(flags, WATCHING) ~= 0 then + if bit32.btest(flags, WATCHING) then notify(sub) end - if bit32.band(flags, MUTABLE) ~= 0 then + if bit32.btest(flags, MUTABLE) then local subSubs = sub.subs if subSubs then link = subSubs @@ -257,7 +257,7 @@ local function createReactiveSystem(props: { local subFlags = sub.flags if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then sub.flags = bit32.bor(subFlags, DIRTY) - if bit32.band(subFlags, WATCHING) ~= 0 then + if bit32.btest(subFlags, WATCHING) then notify(sub) end end @@ -275,7 +275,7 @@ local function createReactiveSystem(props: { local dirty = false - if bit32.band(sub.flags, DIRTY) ~= 0 then + if bit32.btest(sub.flags, DIRTY) then dirty = true elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then if update(dep) then diff --git a/packages/charm/src/types.luau b/packages/charm/src/types.luau index 7775ece..d5c0314 100644 --- a/packages/charm/src/types.luau +++ b/packages/charm/src/types.luau @@ -1,11 +1,5 @@ -export type Getter = () -> T - -export type Setter = (update: Update) -> T - -export type Update = T | (previous: T) -> T - -export type Selector = () -> T - -export type Atom = (...Update) -> T +type Update = ((previous: T) -> T) | T +export type Atom = (...Update) -> T +export type Selector = () -> T return {} diff --git a/packages/charm/src/untracked.luau b/packages/charm/src/untracked.luau index 503b1bd..b766aae 100644 --- a/packages/charm/src/untracked.luau +++ b/packages/charm/src/untracked.luau @@ -1,21 +1,18 @@ -local noYield = require("./noYield") -local signals = require("./signals") +local core = require("./core") -type ReactiveNode = signals.ReactiveNode +local setCurrentTracking = core.setCurrentTracking -local setCurrentSub = signals.setCurrentSub - -local function handleUntracked(currentSub: ReactiveNode?, ok: boolean, ...) - setCurrentSub(currentSub) +local function handleUntracked(prevTracking: boolean, ok: boolean, ...) + setCurrentTracking(prevTracking) if ok then return ... else - error(...) + error((...)) end end local function untracked(callback: (Args...) -> Results..., ...: Args...): Results... - return handleUntracked(setCurrentSub(nil), noYield(callback, ...)) + return handleUntracked(setCurrentTracking(false), pcall(callback, ...)) end return untracked diff --git a/packages/charm/src/noYield.luau b/packages/charm/src/utils.luau similarity index 62% rename from packages/charm/src/noYield.luau rename to packages/charm/src/utils.luau index d132fe6..536e086 100644 --- a/packages/charm/src/noYield.luau +++ b/packages/charm/src/utils.luau @@ -1,13 +1,11 @@ local function handleNoYield(thread: thread, ok: boolean, ...) if not ok then return false, debug.traceback(thread, tostring((...))) - end - - if coroutine.status(thread) ~= "dead" then + elseif coroutine.status(thread) ~= "dead" then return false, debug.traceback(thread, "Attempted to yield!") + else + return true, ... end - - return true, ... end local function noYield(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...) @@ -15,6 +13,6 @@ local function noYield(callback: (Args...) -> Results..., . return handleNoYield(thread, coroutine.resume(thread, ...)) end --- To eliminate the overhead of noYield returning callback(...) in production, --- replace it with pcall to simulate the same behavior. -return if _G.__DEV__ then noYield else pcall :: never +return { + noYield = noYield :: typeof(pcall), +} diff --git a/packages/charm/test/default.project.json b/packages/charm/test/default.project.json new file mode 100644 index 0000000..4cd33dd --- /dev/null +++ b/packages/charm/test/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "charm", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/packages/charm/test/src/batched.luau b/packages/charm/test/src/batched.luau new file mode 100644 index 0000000..685365a --- /dev/null +++ b/packages/charm/test/src/batched.luau @@ -0,0 +1,20 @@ +local core = require("./core") + +local startBatch = core.startBatch +local endBatch = core.endBatch + +local function handleBatched(ok: boolean, ...) + endBatch() + if ok then + return ... + else + error((...)) + end +end + +local function batched(callback: (Args...) -> Results..., ...: Args...): Results... + startBatch() + return handleBatched(pcall(callback, ...)) +end + +return batched diff --git a/packages/charm/test/src/core.luau b/packages/charm/test/src/core.luau new file mode 100644 index 0000000..75c8319 --- /dev/null +++ b/packages/charm/test/src/core.luau @@ -0,0 +1,445 @@ +--[[ + alien-signals 2.0.5 implementation adopted from + https://github.com/Nicell/alien-signals-luau + https://github.com/stackblitz/alien-signals + + MIT License + + Copyright (c) 2024-present Johnson Chu, Nicholas Winans + + 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. +]] + +local strict = require("./strict") +local system = require("./system") +local utils = require("./utils") + +export type ReactiveFlags = system.ReactiveFlags +export type ReactiveNode = system.ReactiveNode +export type Link = system.Link + +local ReactiveFlags = system.ReactiveFlags +local createReactiveSystem = system.createReactiveSystem +local noYield = utils.noYield + +local NONE = 0b0000000 +local MUTABLE = 0b0000001 +local WATCHING = 0b0000010 +local DIRTY = 0b0010000 +local PENDING = 0b0100000 +local QUEUED = 0b1000000 + +type EffectScope = ReactiveNode & { + cleanups: { () -> () }, +} + +type Effect = ReactiveNode & { + fn: () -> ...(() -> ())?, + cleanups: { () -> () }, +} + +type Computed = ReactiveNode & { + value: T?, + getter: (previousValue: T?) -> T, + equals: ((T, T) -> boolean)?, +} + +type Atom = ReactiveNode & { + previousValue: T, + value: T, + equals: ((T, T) -> boolean)?, +} + +local link: (dep: ReactiveNode, sub: ReactiveNode) -> () +local unlink: (link: Link, sub: ReactiveNode?) -> Link? +local propagate: (link: Link) -> () +local checkDirty: (link: Link, sub: ReactiveNode) -> boolean +local endTracking: (sub: ReactiveNode) -> () +local startTracking: (sub: ReactiveNode) -> () +local shallowPropagate: (link: Link) -> () + +local batchDepth = 0 +local queuedEffects: { Effect | EffectScope? } = {} +local queuedEffectsLength = 0 +local notifyIndex = 0 +local activeSubTracking = true +local activeSub: ReactiveNode? +local activeScope: Effect | EffectScope? + +local function getCurrentSub(): ReactiveNode? + return activeSub +end + +local function setCurrentSub(sub: ReactiveNode?) + local prevSub = activeSub + activeSub = sub + return prevSub +end + +local function getCurrentScope(): Effect | EffectScope? + return activeScope +end + +local function setCurrentScope(scope: Effect | EffectScope?) + local prevScope = activeScope + activeScope = scope + return prevScope +end + +local function setCurrentTracking(tracking: boolean): boolean + local prevTracking = activeSubTracking + activeSubTracking = tracking + return prevTracking +end + +local function runCleanups(effect: EffectScope) + for _, cleanup in effect.cleanups do + local prev = setCurrentSub(nil) + local success, result: unknown = (if strict.enabled then noYield else pcall)(cleanup) + setCurrentSub(prev) + if not success then + error(result) + end + end + table.clear(effect.cleanups) +end + +local function onCleanup(cleanup: () -> (), failSilently: boolean?) + if activeSub then + table.insert((activeSub :: Effect).cleanups, cleanup) + elseif activeScope then + table.insert(activeScope.cleanups, cleanup) + elseif not failSilently then + warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2)) + end +end + +local function run(effect: Effect | EffectScope, flags: ReactiveFlags): () + if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then + runCleanups(effect) + local prev = setCurrentSub(effect) + local prevTracking = setCurrentTracking(true) + startTracking(effect) + local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)((effect :: Effect).fn) + setCurrentSub(prev) + endTracking(effect) + setCurrentTracking(prevTracking) + if not success then + error(result) + elseif result then + table.insert(effect.cleanups, result) + end + return + elseif bit32.btest(flags, PENDING) then + (effect :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING)) + end + local link = effect.deps + while link do + local dep = link.dep + local depFlags = dep.flags + if bit32.btest(depFlags, QUEUED) then + depFlags = bit32.band(depFlags, bit32.bnot(QUEUED)) + dep.flags = depFlags + run(dep :: Effect, depFlags) + end + link = link.nextDep + end +end + +local function flush(): () + while notifyIndex < queuedEffectsLength do + notifyIndex += 1 + local effect = queuedEffects[notifyIndex] :: Effect + queuedEffects[notifyIndex] = nil + effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED)) + run(effect, effect.flags) + end + notifyIndex = 0 + queuedEffectsLength = 0 +end + +local function getBatchDepth(): number + return batchDepth +end + +local function startBatch() + batchDepth += 1 +end + +local function endBatch() + batchDepth -= 1 + if batchDepth == 0 then + flush() + end +end + +local function updateComputed(computed: Computed): boolean + local prevSub = setCurrentSub(computed) + local prevTracking = setCurrentTracking(true) + startTracking(computed) + local oldValue = computed.value + local success, result = (if strict.enabled then noYield else pcall)(computed.getter, oldValue) + setCurrentSub(prevSub) + endTracking(computed) + setCurrentTracking(prevTracking) + if not success then + error(result) + end + local equals = computed.equals + if if equals then not equals(oldValue, result) else oldValue ~= result then + computed.value = result + return true + else + return false + end +end + +local function updateAtom(atom: Atom, value: any): boolean + atom.flags = MUTABLE + if atom.previousValue ~= value then + atom.previousValue = value + return true + else + return false + end +end + +local function readAtom(atom: Atom): T + local value = atom.value + if bit32.btest(atom.flags, DIRTY) then + if updateAtom(atom, value) then + local subs = atom.subs + if subs then + shallowPropagate(subs) + end + end + end + if activeSub and activeSubTracking then + link(atom, activeSub) + end + return value +end + +local function writeAtom(atom: Atom, oldValue: T, newValue: T): T + local equals = atom.equals + if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then + atom.value = newValue + atom.flags = bit32.bor(MUTABLE, DIRTY) + local subs = atom.subs + if subs then + propagate(subs) + if batchDepth == 0 then + flush() + end + end + return newValue + end + return oldValue +end + +local function atomGetterSetter(atom: Atom): (...((T) -> T) | T) -> T + return function(...) + if select("#", ...) == 0 then + return readAtom(atom) + end + local oldValue = atom.value + local newValue = ... + if type(newValue) == "function" then + local prev = setCurrentSub(nil) + local success, result = (if strict.enabled then noYield else pcall)(newValue, oldValue) + setCurrentSub(prev) + if not success then + error(result) + end + newValue = result + end + return writeAtom(atom, oldValue, newValue :: T) + end +end + +local function computedGetter(computed: Computed): () -> T + return function() + local flags = computed.flags + if + bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed)) + then + if updateComputed(computed) then + local subs = computed.subs + if subs then + shallowPropagate(subs) + end + end + elseif bit32.btest(flags, PENDING) then + computed.flags = bit32.band(flags, bit32.bnot(PENDING)) + end + if activeSub then + if activeSubTracking then + link(computed, activeSub) + end + elseif activeScope then + link(computed, activeScope) + end + return computed.value :: T + end +end + +local function effectStop(self: Effect | EffectScope): () -> () + return function() + local dep = self.deps + while dep do + dep = unlink(dep, self) + end + local sub = self.subs + if sub then + unlink(sub) + end + (self :: Effect).flags = NONE + runCleanups(self) + end +end + +local function update(atom: Atom | Computed): boolean + if (atom :: Computed).getter then + return updateComputed(atom :: Computed) + else + return updateAtom(atom :: Atom, atom.value) + end +end + +local function notify(effect: Effect | EffectScope) + local flags = effect.flags + if not bit32.btest(flags, QUEUED) then + (effect :: Effect).flags = bit32.bor(flags, QUEUED) + local subs = effect.subs + if subs then + notify(subs.sub :: Effect) + else + queuedEffectsLength += 1 + queuedEffects[queuedEffectsLength] = effect + end + end +end + +local function unwatched(node: Atom | Computed | Effect | EffectScope) + if (node :: Computed).getter then + local toRemove = node.deps + if toRemove then + (node :: Computed).flags = bit32.bor(MUTABLE, DIRTY) + repeat + toRemove = unlink(toRemove, node) + until not toRemove + end + elseif (node :: Effect).cleanups then + effectStop(node :: Effect)() + end +end + +local function atom(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T + return atomGetterSetter({ + previousValue = initialValue, + value = initialValue, + flags = MUTABLE, + equals = equals, + }) +end + +local function computed(getter: (previousValue: T?) -> T, equals: ((T, T) -> boolean)?): () -> T + return computedGetter({ + flags = bit32.bor(MUTABLE, DIRTY), + getter = getter, + equals = equals, + }) +end + +local function effect(fn: () -> ...(() -> ())?): () -> () + local self: Effect = { + fn = fn, + flags = WATCHING, + cleanups = {}, + } + if activeSub then + link(self, activeSub) + elseif activeScope then + link(self, activeScope) + end + local prev = setCurrentSub(self) + local prevTracking = setCurrentTracking(true) + local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn) + setCurrentSub(prev) + setCurrentTracking(prevTracking) + if not success then + error(result) + elseif result then + table.insert(self.cleanups, result) + end + return effectStop(self) +end + +local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> () + local self: EffectScope = { + flags = NONE, + cleanups = {}, + } + if activeScope and not detached then + link(self, activeScope) + end + local prevSub = setCurrentSub(nil) + local prevScope = setCurrentScope(self) + local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn) + setCurrentScope(prevScope) + setCurrentSub(prevSub) + if not success then + error(result) + elseif result then + table.insert(self.cleanups, result) + end + return effectStop(self) +end + +local reactiveSystem = createReactiveSystem({ + update = update :: (ReactiveNode) -> boolean, + notify = notify :: (ReactiveNode) -> (), + unwatched = unwatched :: (ReactiveNode) -> (), +}) + +link = reactiveSystem.link +unlink = reactiveSystem.unlink +propagate = reactiveSystem.propagate +checkDirty = reactiveSystem.checkDirty +endTracking = reactiveSystem.endTracking +startTracking = reactiveSystem.startTracking +shallowPropagate = reactiveSystem.shallowPropagate + +return { + ReactiveFlags = ReactiveFlags, + createReactiveSystem = createReactiveSystem, + getCurrentSub = getCurrentSub, + setCurrentSub = setCurrentSub, + getCurrentScope = getCurrentScope, + setCurrentScope = setCurrentScope, + setCurrentTracking = setCurrentTracking, + onCleanup = onCleanup, + getBatchDepth = getBatchDepth, + startBatch = startBatch, + endBatch = endBatch, + atom = atom, + computed = computed, + effect = effect, + effectScope = effectScope, +} diff --git a/packages/charm/test/src/init.luau b/packages/charm/test/src/init.luau new file mode 100644 index 0000000..e583ae6 --- /dev/null +++ b/packages/charm/test/src/init.luau @@ -0,0 +1,34 @@ +local batched = require("@self/batched") +local core = require("@self/core") +local listen = require("@self/listen") +local mapped = require("@self/mapped") +local observe = require("@self/observe") +local strict = require("@self/strict") +local subscribe = require("@self/subscribe") +local types = require("@self/types") +local untracked = require("@self/untracked") + +export type Atom = types.Atom +export type Selector = types.Selector + +local atom = core.atom +local computed = core.computed +local effect = core.effect +local effectScope = core.effectScope +local onCleanup = core.onCleanup + +return { + core = core, + strict = strict, + atom = atom, + batched = batched, + computed = computed, + effect = effect, + effectScope = effectScope, + listen = listen, + mapped = mapped, + observe = observe, + onCleanup = onCleanup, + subscribe = subscribe, + untracked = untracked, +} diff --git a/packages/charm/test/src/listen.luau b/packages/charm/test/src/listen.luau new file mode 100644 index 0000000..865e704 --- /dev/null +++ b/packages/charm/test/src/listen.luau @@ -0,0 +1,32 @@ +local core = require("./core") +local untracked = require("./untracked") + +local computed = core.computed +local effect = core.effect + +local function listen(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> () + local tracked = computed(function() + return selector() + end) + + local current: T + local disposed = false + + local function dispose() + disposed = true + end + + dispose = effect(function() + local previous = current + current = tracked() + untracked(listener, current, previous, dispose) + end) + + if disposed then + dispose() + end + + return dispose +end + +return listen diff --git a/packages/charm/test/src/mapped.luau b/packages/charm/test/src/mapped.luau new file mode 100644 index 0000000..f6289be --- /dev/null +++ b/packages/charm/test/src/mapped.luau @@ -0,0 +1,59 @@ +local core = require("./core") + +local computed = core.computed +local setCurrentSub = core.setCurrentSub + +type Mapped = + & ((selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO }) + & ((selector: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO }) + +local function mapped(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO } + local currentData: { [KI]: VI } = {} + local keyCache: { [KI]: KO } = {} + + local function map(value: VI, key: KI): (VO, KO) + local currentSub = setCurrentSub(nil) + local success, valueOut, keyOut = pcall(mapper, value, key) + setCurrentSub(currentSub) + + if not success then + error(`mapped() transform function failed: {valueOut}`) + end + + return valueOut, if keyOut == nil then key else keyOut :: any + end + + return computed(function(previousOutput) + local currentOutput: { [KO]: VO } = previousOutput or {} + local previousData = currentData + currentData = selector() + + for key in previousData do + if currentData[key] ~= nil then + continue + end + local cachedKey = keyCache[key] + if cachedKey then + currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput + currentOutput[cachedKey] = nil + keyCache[key] = nil + end + end + + for key, value in currentData do + if value == previousData[key] then + continue + end + local valueOut, keyOut = map(value, key) + if currentOutput[keyOut] ~= valueOut then + currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput + currentOutput[keyOut] = valueOut + keyCache[key] = keyOut + end + end + + return currentOutput + end) +end + +return mapped :: Mapped diff --git a/packages/charm/test/src/observe.luau b/packages/charm/test/src/observe.luau new file mode 100644 index 0000000..7272584 --- /dev/null +++ b/packages/charm/test/src/observe.luau @@ -0,0 +1,52 @@ +local core = require("./core") + +local effect = core.effect +local effectScope = core.effectScope +local onCleanup = core.onCleanup + +local function observe(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> () + local scopes: { [K]: () -> () } = {} + local currentItems: { [K]: V } = {} + + local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V }) + for key in previousItems do + local dispose = scopes[key] + if currentItems[key] == nil and dispose then + dispose() + scopes[key] = nil + end + end + + for key, value in currentItems do + if previousItems[key] == nil then + scopes[key] = effectScope(function() + return observer(value, key) + end, true) + end + end + end + + local function disposeScopes() + for _, dispose in scopes do + dispose() + end + table.clear(scopes) + end + + local disposeEffect = effect(function() + local previousItems = currentItems + currentItems = selector() + if currentItems ~= previousItems then + updateScopes(currentItems, previousItems) + end + end) + + onCleanup(disposeScopes, true) + + return function() + disposeEffect() + disposeScopes() + end +end + +return observe diff --git a/packages/charm/test/src/strict.luau b/packages/charm/test/src/strict.luau new file mode 100644 index 0000000..1955aa1 --- /dev/null +++ b/packages/charm/test/src/strict.luau @@ -0,0 +1,8 @@ +local function inlineTest(): string + return debug.info(1, "n") +end + +return { + -- Enable strict mode in O2 + enabled = inlineTest() == "inlineTest", +} diff --git a/packages/charm/test/src/subscribe.luau b/packages/charm/test/src/subscribe.luau new file mode 100644 index 0000000..1ceca66 --- /dev/null +++ b/packages/charm/test/src/subscribe.luau @@ -0,0 +1,26 @@ +local core = require("./core") +local untracked = require("./untracked") + +local computed = core.computed +local effect = core.effect + +local function subscribe(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> () + local tracked = computed(function() + return selector() + end) + + local current: T + local mounted = true + + return effect(function() + local previous = current + current = tracked() + if mounted then + mounted = false + else + untracked(listener, current, previous) + end + end) +end + +return subscribe diff --git a/packages/charm/test/src/system.luau b/packages/charm/test/src/system.luau new file mode 100644 index 0000000..d3d5692 --- /dev/null +++ b/packages/charm/test/src/system.luau @@ -0,0 +1,357 @@ +--[[ + alien-signals 2.0.1 implementation adopted from + https://github.com/Nicell/alien-signals-luau + https://github.com/stackblitz/alien-signals + + MIT License + + Copyright (c) 2024-present Johnson Chu, Nicholas Winans + + 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. +]] + +export type ReactiveNode = { + deps: Link?, + depsTail: Link?, + subs: Link?, + subsTail: Link?, + flags: ReactiveFlags, +} + +export type Link = { + dep: ReactiveNode, + sub: ReactiveNode, + prevSub: Link?, + nextSub: Link?, + prevDep: Link?, + nextDep: Link?, +} + +type Stack = { + value: T, + prev: Stack?, +} + +export type ReactiveFlags = number + +local NONE = 0b0000000 +local MUTABLE = 0b0000001 +local WATCHING = 0b0000010 +local RECURSED_CHECK = 0b0000100 +local RECURSED = 0b0001000 +local DIRTY = 0b0010000 +local PENDING = 0b0100000 + +local ReactiveFlags = { + None = NONE, + Mutable = MUTABLE, + Watching = WATCHING, + RecursedCheck = RECURSED_CHECK, + Recursed = RECURSED, + Dirty = DIRTY, + Pending = PENDING, +} + +local function createReactiveSystem(props: { + update: (sub: ReactiveNode) -> boolean, + notify: (sub: ReactiveNode) -> (), + unwatched: (sub: ReactiveNode) -> (), +}) + local update = props.update + local notify = props.notify + local unwatched = props.unwatched + + local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean + local depsTail = sub.depsTail + if depsTail then + local link = sub.deps :: Link + repeat + if link == checkLink then + return true + end + if link == depsTail then + break + end + link = link.nextDep :: Link + until not link + end + return false + end + + local function link(dep: ReactiveNode, sub: ReactiveNode): () + local prevDep = sub.depsTail + if prevDep and prevDep.dep == dep then + return + end + local nextDep: Link? + local recursedCheck = bit32.btest(sub.flags, RECURSED_CHECK) + if recursedCheck then + nextDep = if prevDep then prevDep.nextDep else sub.deps + if nextDep and nextDep.dep == dep then + sub.depsTail = nextDep + return + end + end + local prevSub = dep.subsTail + if prevSub and prevSub.sub == sub and (not recursedCheck or isValidLink(prevSub, sub)) then + return + end + local newLink: Link = { + dep = dep, + sub = sub, + prevDep = prevDep, + nextDep = nextDep, + prevSub = prevSub, + } + sub.depsTail = newLink + dep.subsTail = newLink + if nextDep then + nextDep.prevDep = newLink + end + if prevDep then + prevDep.nextDep = newLink + else + sub.deps = newLink + end + if prevSub then + prevSub.nextSub = newLink + else + dep.subs = newLink + end + end + + local function unlink(link: Link, sub: ReactiveNode?): Link? + local sub = sub or link.sub + local dep = link.dep + local prevDep = link.prevDep + local nextDep = link.nextDep + local nextSub = link.nextSub + local prevSub = link.prevSub + if nextDep then + nextDep.prevDep = prevDep + else + sub.depsTail = prevDep + end + if prevDep then + prevDep.nextDep = nextDep + else + sub.deps = nextDep + end + if nextSub then + nextSub.prevSub = prevSub + else + dep.subsTail = prevSub + end + if prevSub then + prevSub.nextSub = nextSub + else + dep.subs = nextSub + if not nextSub then + unwatched(dep) + end + end + return nextDep + end + + local function propagate(link: Link): () + local next = link.nextSub + local stack: Stack? + + repeat + local sub = link.sub + + local flags = sub.flags + + if bit32.btest(flags, bit32.bor(MUTABLE, WATCHING)) then + if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then + sub.flags = bit32.bor(flags, PENDING) + elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then + flags = NONE + elseif not bit32.btest(flags, RECURSED_CHECK) then + sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING) + elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then + sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING)) + flags = bit32.band(flags, MUTABLE) + else + -- selene: allow(if_same_then_else) + flags = NONE + end + + if bit32.btest(flags, WATCHING) then + notify(sub) + end + + if bit32.btest(flags, MUTABLE) then + local subSubs = sub.subs + if subSubs then + link = subSubs + if subSubs.nextSub then + stack = { value = next, prev = stack } + next = link.nextSub + end + continue + end + end + end + + link = next :: Link + + if link then + next = link.nextSub + continue + end + + local continueTop = false + + while stack do + link = stack.value :: Link + stack = stack.prev + if link then + next = link.nextSub + continueTop = true + break + end + end + + if continueTop then + continue + end + + break + until false + end + + local function startTracking(sub: ReactiveNode): () + sub.depsTail = nil + sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK) + end + + local function endTracking(sub: ReactiveNode): () + local depsTail = sub.depsTail + local toRemove = if depsTail then depsTail.nextDep else sub.deps + while toRemove do + toRemove = unlink(toRemove, sub) + end + sub.flags = bit32.band(sub.flags, bit32.bnot(RECURSED_CHECK)) + end + + local function shallowPropagate(link: Link): () + repeat + local sub = link.sub + local subFlags = sub.flags + if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then + sub.flags = bit32.bor(subFlags, DIRTY) + if bit32.btest(subFlags, WATCHING) then + notify(sub) + end + end + link = link.nextSub :: Link + until not link + end + + local function checkDirty(link: Link, sub: ReactiveNode): boolean + local stack: Stack? + local checkDepth = 0 + + repeat + local dep = link.dep + local depFlags = dep.flags + + local dirty = false + + if bit32.btest(sub.flags, DIRTY) then + dirty = true + elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then + if update(dep) then + local subs = dep.subs :: Link + if subs.nextSub then + shallowPropagate(subs) + end + dirty = true + end + elseif bit32.band(depFlags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then + if link.nextSub or link.prevSub then + stack = { value = link, prev = stack } + end + link = dep.deps :: Link + sub = dep + checkDepth += 1 + continue + end + + if not dirty and link.nextDep then + link = link.nextDep + continue + end + + local continueTop = false + + while checkDepth ~= 0 do + checkDepth -= 1 + local firstSub = sub.subs :: Link + local hasMultipleSubs = firstSub.nextSub ~= nil + if hasMultipleSubs then + link = (stack :: Stack).value + stack = (stack :: Stack).prev + else + link = firstSub + end + if dirty then + if update(sub) then + if hasMultipleSubs then + shallowPropagate(firstSub) + end + sub = link.sub + continue + end + else + sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING)) + end + sub = link.sub + if link.nextDep then + link = link.nextDep + continueTop = true + break + end + dirty = false + end + + if continueTop then + continue + end + + return dirty + until false + end + + return { + link = link, + unlink = unlink, + propagate = propagate, + checkDirty = checkDirty, + endTracking = endTracking, + startTracking = startTracking, + shallowPropagate = shallowPropagate, + } +end + +return { + ReactiveFlags = ReactiveFlags, + createReactiveSystem = createReactiveSystem, +} diff --git a/packages/charm/test/src/types.luau b/packages/charm/test/src/types.luau new file mode 100644 index 0000000..d5c0314 --- /dev/null +++ b/packages/charm/test/src/types.luau @@ -0,0 +1,5 @@ +type Update = ((previous: T) -> T) | T +export type Atom = (...Update) -> T +export type Selector = () -> T + +return {} diff --git a/packages/charm/test/src/untracked.luau b/packages/charm/test/src/untracked.luau new file mode 100644 index 0000000..b766aae --- /dev/null +++ b/packages/charm/test/src/untracked.luau @@ -0,0 +1,18 @@ +local core = require("./core") + +local setCurrentTracking = core.setCurrentTracking + +local function handleUntracked(prevTracking: boolean, ok: boolean, ...) + setCurrentTracking(prevTracking) + if ok then + return ... + else + error((...)) + end +end + +local function untracked(callback: (Args...) -> Results..., ...: Args...): Results... + return handleUntracked(setCurrentTracking(false), pcall(callback, ...)) +end + +return untracked diff --git a/packages/charm/test/src/utils.luau b/packages/charm/test/src/utils.luau new file mode 100644 index 0000000..536e086 --- /dev/null +++ b/packages/charm/test/src/utils.luau @@ -0,0 +1,18 @@ +local function handleNoYield(thread: thread, ok: boolean, ...) + if not ok then + return false, debug.traceback(thread, tostring((...))) + elseif coroutine.status(thread) ~= "dead" then + return false, debug.traceback(thread, "Attempted to yield!") + else + return true, ... + end +end + +local function noYield(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...) + local thread = coroutine.create(callback) + return handleNoYield(thread, coroutine.resume(thread, ...)) +end + +return { + noYield = noYield :: typeof(pcall), +} diff --git a/packages/charm/test/wally.toml b/packages/charm/test/wally.toml new file mode 100644 index 0000000..cd94b61 --- /dev/null +++ b/packages/charm/test/wally.toml @@ -0,0 +1,8 @@ +[package] +name = "littensy/charm" +version = "0.10.0" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" +exclude = ["node_modules", "src/__tests__", "src/index.d.ts", "package.json"] + +[dependencies] diff --git a/packages/charm/tests/signals/spec.luau b/packages/charm/tests/signals/spec.luau deleted file mode 100644 index 7d5bdd7..0000000 --- a/packages/charm/tests/signals/spec.luau +++ /dev/null @@ -1,27 +0,0 @@ -local suite = require("@test/suite") - -suite.describe("signals", function() - suite.describe("computed", function() - require("./computed.spec") - end) - suite.describe("effect", function() - require("./effect.spec") - end) - suite.describe("effectScope", function() - require("./effectScope.spec") - end) - suite.describe("issue_48", function() - require("./issue_48.spec") - end) - suite.describe("gc", function() - require("./gc.spec") - end) - suite.describe("topology", function() - require("./topology.spec") - end) - suite.describe("untrack", function() - require("./untrack.spec") - end) -end) - -return {} diff --git a/packages/charm/tests/spec.luau b/packages/charm/tests/spec.luau deleted file mode 100644 index a2b8653..0000000 --- a/packages/charm/tests/spec.luau +++ /dev/null @@ -1,23 +0,0 @@ -local suite = require("@test/suite") - -require("./signals/spec") - -suite.describe("charm", function() - suite.describe("listen", function() - require("./listen.spec") - end) - suite.describe("mapped", function() - require("./mapped.spec") - end) - suite.describe("observe", function() - require("./observe.spec") - end) - suite.describe("subscribe", function() - require("./subscribe.spec") - end) - suite.describe("untracked", function() - require("./untracked.spec") - end) -end) - -return {} diff --git a/packages/charm/wally.toml b/packages/charm/wally.toml index 1e798ac..cd94b61 100644 --- a/packages/charm/wally.toml +++ b/packages/charm/wally.toml @@ -3,6 +3,6 @@ name = "littensy/charm" version = "0.10.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" -exclude = ["tests"] +exclude = ["node_modules", "src/__tests__", "src/index.d.ts", "package.json"] [dependencies] diff --git a/packages/react-charm/Charm.luau b/packages/react-charm/Charm.luau new file mode 100644 index 0000000..6b6fb92 --- /dev/null +++ b/packages/react-charm/Charm.luau @@ -0,0 +1,6 @@ +local Charm = require(script.Parent.Parent.charm.src) + +export type Atom = Charm.Atom +export type Selector = Charm.Selector + +return Charm diff --git a/packages/react-charm/React.luau b/packages/react-charm/React.luau new file mode 100644 index 0000000..dd59e59 --- /dev/null +++ b/packages/react-charm/React.luau @@ -0,0 +1,12 @@ +type CoreReactBinding = { + getValue: (self: CoreReactBinding) -> T, + _source: string?, +} + +type ReactBindingMap = { + map: (self: CoreReactBinding & ReactBindingMap, (T) -> U) -> ReactBindingMap & CoreReactBinding, +} + +export type Binding = CoreReactBinding & ReactBindingMap + +return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").React) diff --git a/packages/react-charm/ReactRoblox.luau b/packages/react-charm/ReactRoblox.luau new file mode 100644 index 0000000..0350e6b --- /dev/null +++ b/packages/react-charm/ReactRoblox.luau @@ -0,0 +1 @@ +return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").ReactRoblox) diff --git a/packages/react-charm/default.project.json b/packages/react-charm/default.project.json index c1ee26a..baf768c 100644 --- a/packages/react-charm/default.project.json +++ b/packages/react-charm/default.project.json @@ -1,5 +1,5 @@ { - "name": "ReactCharm", + "name": "react-charm", "tree": { "$path": "src" } diff --git a/packages/react-charm/package.json b/packages/react-charm/package.json new file mode 100644 index 0000000..eeaeefb --- /dev/null +++ b/packages/react-charm/package.json @@ -0,0 +1,30 @@ +{ + "name": "@rbxts/react-charm", + "version": "0.3.0", + "description": "React bindings for Charm state management", + "license": "MIT", + "main": "src/init.luau", + "types": "src/index.d.ts", + "files": [ + "src", + "*.luau", + "!wally.toml", + "!wally.lock", + "!default.project.json", + "!**/__tests__" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/littensy/charm.git" + }, + "scripts": { + "prepare": "cp ../../LICENSE.md . && cp ../../README.md .", + "publish:wally": "wally publish" + }, + "dependencies": { + "@rbxts/charm": "workspace:*" + }, + "peerDependencies": { + "@rbxts/react": "*" + } +} diff --git a/packages/react-charm/tests/helpers/renderHook.luau b/packages/react-charm/src/__tests__/helpers/renderHook.luau similarity index 79% rename from packages/react-charm/tests/helpers/renderHook.luau rename to packages/react-charm/src/__tests__/helpers/renderHook.luau index b4f7ac0..664b2fc 100644 --- a/packages/react-charm/tests/helpers/renderHook.luau +++ b/packages/react-charm/src/__tests__/helpers/renderHook.luau @@ -1,9 +1,7 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") - _G.__ROACT_17_MOCK_SCHEDULER__ = true -local React = require(ReplicatedStorage.Packages.React) -local ReactRoblox = require(ReplicatedStorage.Packages.ReactRoblox) +local React = require("../../../React") +local ReactRoblox = require("../../../ReactRoblox") type RenderedHook = { result: Result, diff --git a/packages/react-charm/tests/spec.luau b/packages/react-charm/src/__tests__/spec.luau similarity index 63% rename from packages/react-charm/tests/spec.luau rename to packages/react-charm/src/__tests__/spec.luau index dabaab7..37326fa 100644 --- a/packages/react-charm/tests/spec.luau +++ b/packages/react-charm/src/__tests__/spec.luau @@ -1,8 +1,9 @@ -local ServerScriptService = game:GetService("ServerScriptService") - -local suite = require(ServerScriptService.Tests.suite) +local suite = require("../../../../test/suite") suite.describe("react-charm", function() + if not game then + return suite.skip() + end suite.describe("useAtom", function() require("./useAtom.spec") end) diff --git a/packages/react-charm/tests/useAtom.spec.luau b/packages/react-charm/src/__tests__/useAtom.spec.luau similarity index 72% rename from packages/react-charm/tests/useAtom.spec.luau rename to packages/react-charm/src/__tests__/useAtom.spec.luau index 3db371e..2df0f78 100644 --- a/packages/react-charm/tests/useAtom.spec.luau +++ b/packages/react-charm/src/__tests__/useAtom.spec.luau @@ -1,10 +1,7 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") - -local Charm = require(ReplicatedStorage.Packages.Charm) -local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm) -local renderHook = require(script.Parent.helpers.renderHook) -local suite = require(ServerScriptService.Tests.suite) +local Charm = require("../../Charm") +local ReactCharm = require("../") +local renderHook = require("./helpers/renderHook") +local suite = require("../../../../test/suite") local atom = Charm.atom local useAtom = ReactCharm.useAtom diff --git a/packages/react-charm/tests/useAtomBinding.spec.luau b/packages/react-charm/src/__tests__/useAtomBinding.spec.luau similarity index 74% rename from packages/react-charm/tests/useAtomBinding.spec.luau rename to packages/react-charm/src/__tests__/useAtomBinding.spec.luau index 28dcad2..e96d3b3 100644 --- a/packages/react-charm/tests/useAtomBinding.spec.luau +++ b/packages/react-charm/src/__tests__/useAtomBinding.spec.luau @@ -1,10 +1,7 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") - -local Charm = require(ReplicatedStorage.Packages.Charm) -local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm) -local renderHook = require(script.Parent.helpers.renderHook) -local suite = require(ServerScriptService.Tests.suite) +local Charm = require("../../Charm") +local ReactCharm = require("../") +local renderHook = require("./helpers/renderHook") +local suite = require("../../../../test/suite") local atom = Charm.atom local useAtomBinding = ReactCharm.useAtomBinding diff --git a/packages/react-charm/src/index.d.ts b/packages/react-charm/src/index.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/react-charm/src/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/react-charm/src/init.luau b/packages/react-charm/src/init.luau index 8fabbb0..0ddbdd0 100644 --- a/packages/react-charm/src/init.luau +++ b/packages/react-charm/src/init.luau @@ -1,5 +1,5 @@ -local Charm = require(script.Parent.Charm) -local React = require(script.Parent.React) +local Charm = require("./Charm") +local React = require("./React") local effect = Charm.effect local useBinding = React.useBinding diff --git a/packages/react-charm/wally.toml b/packages/react-charm/wally.toml index d5dd56e..cc76252 100644 --- a/packages/react-charm/wally.toml +++ b/packages/react-charm/wally.toml @@ -3,7 +3,14 @@ name = "littensy/react-charm" version = "0.3.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" -exclude = ["tests"] +exclude = [ + "node_modules", + "src/__tests__", + "src/index.d.ts", + "package.json", + "React.luau", + "Charm.luau", +] [dependencies] Charm = "littensy/charm@^0.10" diff --git a/packages/vide-charm/Charm.luau b/packages/vide-charm/Charm.luau new file mode 100644 index 0000000..6b6fb92 --- /dev/null +++ b/packages/vide-charm/Charm.luau @@ -0,0 +1,6 @@ +local Charm = require(script.Parent.Parent.charm.src) + +export type Atom = Charm.Atom +export type Selector = Charm.Selector + +return Charm diff --git a/packages/vide-charm/Vide.luau b/packages/vide-charm/Vide.luau new file mode 100644 index 0000000..0c1ac14 --- /dev/null +++ b/packages/vide-charm/Vide.luau @@ -0,0 +1 @@ +return require(script.Parent.Parent.vide.src) diff --git a/packages/vide-charm/default.project.json b/packages/vide-charm/default.project.json index 3a447c6..e2a35ad 100644 --- a/packages/vide-charm/default.project.json +++ b/packages/vide-charm/default.project.json @@ -1,5 +1,5 @@ { - "name": "VideCharm", + "name": "vide-charm", "tree": { "$path": "src" } diff --git a/packages/vide-charm/package.json b/packages/vide-charm/package.json new file mode 100644 index 0000000..4182564 --- /dev/null +++ b/packages/vide-charm/package.json @@ -0,0 +1,30 @@ +{ + "name": "@rbxts/vide-charm", + "version": "0.3.0", + "description": "Vide bindings for Charm state management", + "license": "MIT", + "main": "src/init.luau", + "types": "src/index.d.ts", + "files": [ + "src", + "*.luau", + "!wally.toml", + "!wally.lock", + "!default.project.json", + "!**/__tests__" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/littensy/charm.git" + }, + "scripts": { + "prepare": "cp ../../LICENSE.md . && cp ../../README.md .", + "publish:wally": "wally publish" + }, + "dependencies": { + "@rbxts/charm": "workspace:*" + }, + "peerDependencies": { + "@rbxts/vide": "*" + } +} diff --git a/packages/vide-charm/tests/spec.luau b/packages/vide-charm/src/__tests__/spec.luau similarity index 51% rename from packages/vide-charm/tests/spec.luau rename to packages/vide-charm/src/__tests__/spec.luau index 4583308..c445ac5 100644 --- a/packages/vide-charm/tests/spec.luau +++ b/packages/vide-charm/src/__tests__/spec.luau @@ -1,8 +1,9 @@ -local ServerScriptService = game:GetService("ServerScriptService") - -local suite = require(ServerScriptService.Tests.suite) +local suite = require("../../../../test/suite") suite.describe("vide-charm", function() + if not game then + return suite.skip() + end suite.describe("useAtom", function() require("./useAtom.spec") end) diff --git a/packages/vide-charm/tests/useAtom.spec.luau b/packages/vide-charm/src/__tests__/useAtom.spec.luau similarity index 64% rename from packages/vide-charm/tests/useAtom.spec.luau rename to packages/vide-charm/src/__tests__/useAtom.spec.luau index 84b202d..5f4a801 100644 --- a/packages/vide-charm/tests/useAtom.spec.luau +++ b/packages/vide-charm/src/__tests__/useAtom.spec.luau @@ -1,10 +1,7 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") - -local Charm = require(ReplicatedStorage.Packages.Charm) -local Vide = require(ReplicatedStorage.Packages.Vide) -local VideCharm = require(ReplicatedStorage.Packages.VideCharm) -local suite = require(ServerScriptService.Tests.suite) +local Charm = require("../../Charm") +local Vide = require("../../Vide") +local VideCharm = require("../") +local suite = require("../../../../test/suite") local atom = Charm.atom local root = Vide.root diff --git a/packages/vide-charm/src/index.d.ts b/packages/vide-charm/src/index.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/vide-charm/src/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/vide-charm/src/init.luau b/packages/vide-charm/src/init.luau index 805a634..766e57b 100644 --- a/packages/vide-charm/src/init.luau +++ b/packages/vide-charm/src/init.luau @@ -1,5 +1,5 @@ -local Charm = require(script.Parent.Charm) -local Vide = require(script.Parent.Vide) +local Charm = require("./Charm") +local Vide = require("./Vide") local subscribe = Charm.subscribe local source = Vide.source diff --git a/packages/vide-charm/wally.toml b/packages/vide-charm/wally.toml index dc9d909..9e88a0f 100644 --- a/packages/vide-charm/wally.toml +++ b/packages/vide-charm/wally.toml @@ -3,7 +3,14 @@ name = "littensy/vide-charm" version = "0.3.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" -exclude = ["tests"] +exclude = [ + "node_modules", + "src/__tests__", + "src/index.d.ts", + "package.json", + "Vide.luau", + "Charm.luau", +] [dependencies] Charm = "littensy/charm@^0.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b51612c..ac32a87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,124 @@ importers: '@rbxts-js/react-roblox': specifier: 17.1.20-ts.1 version: 17.1.20-ts.1 + '@rbxts/compiler-types': + specifier: 3.0.0-types.0 + version: 3.0.0-types.0 + '@rbxts/react': + specifier: 17.3.0-alpha.1 + version: 17.3.0-alpha.1 + '@rbxts/react-roblox': + specifier: 17.3.0-alpha.1 + version: 17.3.0-alpha.1(@rbxts/react@17.3.0-alpha.1) + '@rbxts/types': + specifier: ^1.0.861 + version: 1.0.861 '@rbxts/vide': specifier: ^0.5.7 version: 0.5.7 + '@typescript-eslint/eslint-plugin': + specifier: ^8.35.1 + version: 8.35.1(@typescript-eslint/parser@8.35.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.35.1 + version: 8.35.1(eslint@8.57.1)(typescript@5.8.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^10.1.5 + version: 10.1.5(eslint@8.57.1) + eslint-plugin-prettier: + specifier: ^5.5.1 + version: 5.5.1(eslint-config-prettier@10.1.5(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) + eslint-plugin-roblox-ts: + specifier: ^0.0.36 + version: 0.0.36(eslint@8.57.1) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + roblox-ts: + specifier: ^3.0.0 + version: 3.0.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + packages/charm: {} + + packages/charm-sync: + dependencies: + '@rbxts/charm': + specifier: workspace:* + version: link:../charm + + packages/react-charm: + dependencies: + '@rbxts/charm': + specifier: workspace:* + version: link:../charm + '@rbxts/react': + specifier: '*' + version: 17.2.3 + + packages/vide-charm: + dependencies: + '@rbxts/charm': + specifier: workspace:* + version: link:../charm + '@rbxts/vide': + specifier: '*' + version: 0.5.7 packages: + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.2.7': + resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@rbxts-js/boolean@1.2.3-ts.1': resolution: {integrity: sha512-IlYFG9uatXQKXOLR1mdptVeEcxudwBCierO+Vu6VZRRaI+/+JDxUClm+QFZyce/kitwnDTmtNvhqVAUtFvCU0Q==} @@ -74,11 +186,808 @@ packages: '@rbxts-js/timers@1.2.3-ts.1': resolution: {integrity: sha512-HKP4ymFX3QLWbR/2Gy4LXwvkhjC9OzbwZ5bNunxu+M8Ocbi1cjRduiZK/y5ogLr9cNIfSoxs6e1DQ3soc8/hwA==} + '@rbxts/compiler-types@3.0.0-types.0': + resolution: {integrity: sha512-VGOHJPoL7+56NTatMGqQj3K7xWuzEV+aP4QD5vZiHu+bcff3kiTmtoadaF6NkJrmwfFAvbsd4Dg764ZjWNceag==} + + '@rbxts/react-roblox@17.3.0-alpha.1': + resolution: {integrity: sha512-P6vL3+LFC7YZxFGfx3hpVdd4bPBqOkJkDpr0aRpHmVj0OVoVCPiaPXe1qCPp5MciBRb4MQXauJCeiVVs4CvwJg==} + peerDependencies: + '@rbxts/react': ^17.3.0-alpha.1 + + '@rbxts/react-vendor@17.2.3': + resolution: {integrity: sha512-dWCiVelvTK6h920geaFH/OQ5zoWHyhyqyfH51hKx2/iVZ6OiiyNVJ5UqEIkJMG4HsJ8JnkTORizV3+MtQ41BtA==} + + '@rbxts/react@17.2.3': + resolution: {integrity: sha512-IoOB8e2W8MxT0MKWFf0Ls0vT61ImYL6ctHQgz5+oazma6z8bqFtJjgPxOuy6jmMSuyfBe7rYp/muou0kIjZT+Q==} + + '@rbxts/react@17.3.0-alpha.1': + resolution: {integrity: sha512-l2KYYxSj5pOKA2/QelbQRiqQjbI6SRMv8mrmIjc7xpAzuw728aumJ6V6Ol0lrQO/Qt5LVQ8En2L+6C6Lt4sLqA==} + + '@rbxts/types@1.0.861': + resolution: {integrity: sha512-WYIODP9WM7r2uc7+M6PA38yeSKf4bvIrh4MTbk4pvb1vzLwYlnJ3tq9q4SaI5e8BQsNWMSjXcI+s9p4tXY4v5A==} + '@rbxts/vide@0.5.7': resolution: {integrity: sha512-Q9R7ybzuHK1phtLicBHgAjQnlJ/dMsXDs2k296PIMLgLqK9aX/f1skOP7F1K/vo7IrSE0jCG3B3XTOtQL1Sb0Q==} + '@roblox-ts/luau-ast@2.0.0': + resolution: {integrity: sha512-cmMi093IdwBOLVxwuordhM8AmtbyTIyRpsTbB0D/JauidW4SXsQRQowSwWjHo4QP0DRJBXvOIlxtqEQi50uNzQ==} + + '@roblox-ts/path-translator@1.1.0': + resolution: {integrity: sha512-D0akTmnNYqBw+ZIek5JxocT3BjmbgGOuOy0x1nIIxHBPNLGCpzseToY8jyYs/0mlvnN2xnSP/k8Tv+jvGOQSwQ==} + + '@roblox-ts/rojo-resolver@1.1.0': + resolution: {integrity: sha512-QmvVryu1EeME+3QUoG5j/gHGJoJUaffCgZ92mhlG7cJSd1uyhgpY4CNWriZAwZJYkTlzd5Htkpn+18yDFbOFXA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@20.19.4': + resolution: {integrity: sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==} + + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + + '@typescript-eslint/eslint-plugin@8.35.1': + resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.35.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/experimental-utils@5.62.0': + resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/parser@8.35.1': + resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.35.1': + resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/scope-manager@8.35.1': + resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.35.1': + resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/type-utils@8.35.1': + resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/types@8.35.1': + resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.35.1': + resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/utils@8.35.1': + resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/visitor-keys@8.35.1': + resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.5: + resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.1: + resolution: {integrity: sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-roblox-ts@0.0.36: + resolution: {integrity: sha512-vlbgfHY1heWSyDunVX3gIOamCy28KqrJlDkmuDOyYVBo8uLqdvbFkbWkdGxn59Vky1m6pFcvZGU6TObfmwm5Pg==} + engines: {node: '>=0.10.0'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roblox-ts@3.0.0: + resolution: {integrity: sha512-hwAC2frIFlLJOtHd6F+5opMEhBgfAMK9z5l1mP+bykLBbMO5cn1q5lIwhhXbeh9Pq07rlhF8uGHlmeRLPd/3AA==} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.11.8: + resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} + engines: {node: ^14.18.0 || >=16.0.0} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + snapshots: + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pkgr/core@0.2.7': {} + '@rbxts-js/boolean@1.2.3-ts.1': dependencies: '@rbxts-js/collections': 1.2.3-ts.1 @@ -161,4 +1070,774 @@ snapshots: dependencies: '@rbxts-js/collections': 1.2.3-ts.1 + '@rbxts/compiler-types@3.0.0-types.0': {} + + '@rbxts/react-roblox@17.3.0-alpha.1(@rbxts/react@17.3.0-alpha.1)': + dependencies: + '@rbxts-js/react-roblox': 17.1.20-ts.1 + '@rbxts/react': 17.3.0-alpha.1 + + '@rbxts/react-vendor@17.2.3': {} + + '@rbxts/react@17.2.3': + dependencies: + '@rbxts/react-vendor': 17.2.3 + + '@rbxts/react@17.3.0-alpha.1': + dependencies: + '@rbxts-js/react': 17.1.20-ts.1 + + '@rbxts/types@1.0.861': {} + '@rbxts/vide@0.5.7': {} + + '@roblox-ts/luau-ast@2.0.0': {} + + '@roblox-ts/path-translator@1.1.0': + dependencies: + ajv: 8.17.1 + fs-extra: 11.3.0 + + '@roblox-ts/rojo-resolver@1.1.0': + dependencies: + ajv: 8.17.1 + fs-extra: 11.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/node@20.19.4': + dependencies: + undici-types: 6.21.0 + + '@types/semver@7.7.0': {} + + '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.35.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/experimental-utils@5.62.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/parser@8.35.1(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + debug: 4.4.1 + eslint: 8.57.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/scope-manager@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.35.1(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@8.57.1)(typescript@5.8.3) + debug: 4.4.1 + eslint: 8.57.1 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/types@8.35.1': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.1 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.2 + tsutils: 3.21.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.0 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.35.1(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + eslint: 8.57.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + emoji-regex@8.0.0: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.5(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-prettier@5.5.1(eslint-config-prettier@10.1.5(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2): + dependencies: + eslint: 8.57.1 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.8 + optionalDependencies: + eslint-config-prettier: 10.1.5(eslint@8.57.1) + + eslint-plugin-roblox-ts@0.0.36(eslint@8.57.1): + dependencies: + '@types/node': 20.19.4 + '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - eslint + - supports-color + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + natural-compare@1.4.0: {} + + normalize-path@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + picomatch@2.3.1: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + roblox-ts@3.0.0: + dependencies: + '@roblox-ts/luau-ast': 2.0.0 + '@roblox-ts/path-translator': 1.1.0 + '@roblox-ts/rojo-resolver': 1.1.0 + chokidar: 3.6.0 + fs-extra: 11.3.0 + kleur: 4.1.5 + resolve: 1.22.10 + typescript: 5.5.3 + yargs: 17.7.2 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + slash@3.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.11.8: + dependencies: + '@pkgr/core': 0.2.7 + + text-table@0.2.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tslib@1.14.1: {} + + tsutils@3.21.0(typescript@5.8.3): + dependencies: + tslib: 1.14.1 + typescript: 5.8.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.5.3: {} + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..924b55f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/rokit.toml b/rokit.toml index 9412a69..23db8e9 100644 --- a/rokit.toml +++ b/rokit.toml @@ -8,5 +8,6 @@ wally = "UpliftGames/wally@0.3.2" rojo = "rojo-rbx/rojo@7.5.1" stylua = "johnnymorganz/stylua@2.1.0" selene = "Kampfkarren/selene@0.28.0" -luau-lsp = "johnnymorganz/luau-lsp@1.47.0" -lune = "lune-org/lune@0.9.3" +luau-lsp = "johnnymorganz/luau-lsp@1.51.0" +lune = "lune-org/lune@0.9.4" +lute = "luau-lang/lute@0.1.0-nightly.20250701" diff --git a/selene.toml b/selene.toml index 1dab6d8..7da7261 100644 --- a/selene.toml +++ b/selene.toml @@ -1,4 +1,5 @@ std = "selene_defs" +exclude = ["**/node_modules/**"] [lints] global_usage = "allow" diff --git a/test/init.luau b/test/init.luau deleted file mode 100644 index 8048eba..0000000 --- a/test/init.luau +++ /dev/null @@ -1,9 +0,0 @@ -local suite = require("@self/suite") - -if not table.isfrozen(_G) then - _G.__DEV__ = true -end - -require("@packages/charm/tests/spec") - -suite.finish() diff --git a/test/spec.server.luau b/test/spec.server.luau index 6b6956b..b33b7b2 100644 --- a/test/spec.server.luau +++ b/test/spec.server.luau @@ -1,16 +1,11 @@ +local Charm = require("../packages/charm/src") local suite = require("./suite") -if not script then - return -end +Charm.strict.enabled = true -_G.__DEV__ = true +require("../packages/charm/src/__tests__/spec") +require("../packages/charm-sync/src/__tests__/spec") +require("../packages/react-charm/src/__tests__/spec") +require("../packages/vide-charm/src/__tests__/spec") -require(script.Parent.CharmSync.spec) -require(script.Parent.ReactCharm.spec) -require(script.Parent.VideCharm.spec) - -suite.finish({ - colors = false, - verbose = true, -}) +suite.finish() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a323dec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + // required + "allowSyntheticDefaultImports": true, + "downlevelIteration": true, + "jsx": "react", + "jsxFactory": "Roact.createElement", + "jsxFragmentFactory": "Roact.createFragment", + "module": "commonjs", + "moduleResolution": "Node", + "noLib": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "moduleDetection": "force", + "strict": true, + "target": "ESNext", + "typeRoots": ["node_modules/@rbxts"] + } +}