commit 5302ecc6ccacd526c5e8ba19a4670864b9791940 Author: reidlab Date: Tue Jul 18 22:22:52 2023 -0700 first commit diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..d7d0de7 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,48 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "jsx": true, + "useJSXTextNode": true, + "ecmaVersion": 2018, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "ignorePatterns": [ + "/out" + ], + "plugins": [ + "@typescript-eslint", + "roblox-ts" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:roblox-ts/recommended" + ], + "rules": { + "@typescript-eslint/ban-types": "error", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-module-boundary-types": "warn", + "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/no-empty-function": "error", + "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/no-namespace": "error", + "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "varsIgnorePattern": "^_", + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/semi": [ + "error", + "never" + ], + "@typescript-eslint/comma-dangle": [ + "error", + "never" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..046848d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# builds +/out +build.rbxl +*.tsbuildinfo + +# deps +/node_modules +/include + +# mac users.. stop making these files or im gonna get mad 😡😡😡 +.DS_Store +/__MACOSX \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ce11dd2 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "roblox-ts.vscode-roblox-ts", + "dbaeumer.vscode-eslint", + "evaera.vscode-rojo" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..287f952 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "files.eol": "\n", + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "eslint.run": "onType", + "eslint.format.enable": true +} \ No newline at end of file diff --git a/aftman.toml b/aftman.toml new file mode 100644 index 0000000..bf4433e --- /dev/null +++ b/aftman.toml @@ -0,0 +1,7 @@ +# This file lists tools managed by Aftman, a cross-platform toolchain manager. +# For more information, see https://github.com/LPGhatguy/aftman + +# To add a new tool, add an entry to this table. +[tools] +rojo = "rojo-rbx/rojo@7.3.0" +run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" \ No newline at end of file diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000..72dbfce --- /dev/null +++ b/default.project.json @@ -0,0 +1,65 @@ +{ + "name": "roblox-ts-game", + "globIgnorePaths": [ + "**/package.json", + "**/tsconfig.json" + ], + "tree": { + "$className": "DataModel", + "ServerScriptService": { + "$className": "ServerScriptService", + "goopler": { + "$path": "out/src/ServerScriptService" + } + }, + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "rbxts_include": { + "$path": "include", + "node_modules": { + "$className": "Folder", + "@rbxts": { + "$path": "node_modules/@rbxts" + } + } + }, + "goopler": { + "$path": "out/src/ReplicatedStorage" + }, + "tests": { + "$path": "out/tests" + } + }, + "StarterPlayer": { + "$className": "StarterPlayer", + "StarterPlayerScripts": { + "$className": "StarterPlayerScripts", + "goopler": { + "$path": "out/src/StarterPlayer/StarterPlayerScripts" + } + }, + "StarterCharacterScripts": { + "$className": "StarterCharacterScripts", + "$path": "out/src/StarterPlayer/StarterCharacterScripts" + } + }, + "Workspace": { + "$className": "Workspace", + "$properties": { + "FilteringEnabled": true + } + }, + "HttpService": { + "$className": "HttpService", + "$properties": { + "HttpEnabled": true + } + }, + "SoundService": { + "$className": "SoundService", + "$properties": { + "RespectFilteringEnabled": true + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cdbef06 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "goopler", + "description": "An in-dev game that I plan to make a shooter game out of.", + "keywords": ["roblox", "roblox-ts", "rojo", "matter", "roblox-matter", "ecs"], + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "author": { + "name": "reidlab" + }, + "scripts": { + "build": "rbxtsc --rojo default.project.json --verbose && rojo build --output build.rbxl default.project.json", + "watch": "rbxtsc -w --rojo default.project.json --verbose", + "lint": "eslint src tests --max-warnings 0", + "serve": "rojo serve default.project.json", + "test": "yarn run build && run-in-roblox --place build.rbxl --script out/tests/runners/run.server.lua" + }, + "devDependencies": { + "@rbxts/compiler-types": "^2.0.4-types.1", + "@rbxts/types": "^1.0.689", + "@typescript-eslint/eslint-plugin": "^5.59.11", + "@typescript-eslint/parser": "^5.59.11", + "eslint": "^8.42.0", + "eslint-plugin-roblox-ts": "^0.0.35", + "typescript": "^5.1.3" + }, + "dependencies": { + "@rbxts/matter": "^0.6.2-ts.6", + "@rbxts/plasma": "^0.4.1-ts.1", + "@rbxts/rewire": "^0.3.0", + "@rbxts/services": "^1.5.1", + "@rbxts/testez": "^0.4.2-ts.0" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e9b1402 --- /dev/null +++ b/readme.md @@ -0,0 +1,27 @@ +# Goopler + +An in-dev game that I plan to make a shooter game out of. + +--- +> For goop we live, for goop we die. + +— reidlab + +# Hacks +* I get a strange error about private identifiers in [`./src/ReplicatedStorage/ecs/state.ts:7`](./src/ReplicatedStorage/ecs/state.ts) +* I decided to omit the "TS" folder from [`./default.project.json:40`](./default.project.json) due to the script override not working in Health.server.ts in StarterCharacterScripts. + +# Todo +### High priority +* Input mapping +#### Medium priority +* Sprinting (post-input-mapping) +* Add tests +##### Low priority +* Add the bound tags in [`./src/ReplicatedStorage/ecs/boundTags.ts`](./src/ReplicatedStorage/ecs/boundTags.ts) + +# Fixes +### High Priority +* Currently, when resetting, sometimes your health goes back up. This is due to the reconciliation of health. Simply put, your health is not being set to zero inside of our entity component system, due to us not having the reset event currently like that. See it here: [StarterGui.SetCore](https://create.roblox.com/docs/reference/engine/classes/StarterGui#SetCore) It uses BindableEvents and stuff idk +#### Medium priority +##### Low priority \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/boundTags.ts b/src/ReplicatedStorage/ecs/boundTags.ts new file mode 100644 index 0000000..b8f132e --- /dev/null +++ b/src/ReplicatedStorage/ecs/boundTags.ts @@ -0,0 +1,8 @@ +import { Test } from "./components" + +/** + * A map of tags to their bound components. + */ +export const tags = new ReadonlyMap([ + ["Example", Test] +]) \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/components/defaults.ts b/src/ReplicatedStorage/ecs/components/defaults.ts new file mode 100644 index 0000000..a406d4d --- /dev/null +++ b/src/ReplicatedStorage/ecs/components/defaults.ts @@ -0,0 +1,9 @@ +import { Transform } from "./types" + +/** + * The default value created when no data is provided to a {@link Transform} + * component. + */ +export const transform: Transform = { + cframe: CFrame.identity +} \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/components/index.ts b/src/ReplicatedStorage/ecs/components/index.ts new file mode 100644 index 0000000..45c51d7 --- /dev/null +++ b/src/ReplicatedStorage/ecs/components/index.ts @@ -0,0 +1,64 @@ +import { Component, component } from "@rbxts/matter" +import { transform } from "./defaults" +import type { + Model as ModelComponent, + Transform as TransformComponent, + Health as HealthComponent, + Damage as DamageComponent, + PlayerCharacter as PlayerCharacterComponent, + Lifetime as LifetimeComponent, + Input as InputComponent +} from "./types" + +export type GooplerComponentType = + | ModelComponent + | TransformComponent + | HealthComponent + | DamageComponent + | PlayerCharacterComponent + | LifetimeComponent + | InputComponent + +export type GooplerComponent = Component + +/** + * The {@link ModelComponent | Model} component constructor. + */ +export const Model = component("Model") + +/** + * The {@link TransformComponent | Transform} component constructor. + */ +export const Transform = component("Transform", transform) + +/** + * The {@link HealthComponent | Health} component constructor. + */ +export const Health = component("Health") + +/** + * The {@link DamageComponent | Damage} component constructor. + */ +export const Damage = component("Damage") + +/** + * The {@link PlayerCharacterComponent | PlayerCharacter} component constructor. + */ +export const PlayerCharacter = component("PlayerCharacter") + +/** + * The {@link LifetimeComponent | Lifetime} component constructor. + */ +export const Lifetime = component("Lifetime") + +/** + * The {@link InputComponent | Input} component constructor. + */ +export const Input = component("Input") + +/** + * This is a test component constructor. + * + * It shouldn't be used and should be removed at some point. + */ +export const Test = component("Test") \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/components/types.d.ts b/src/ReplicatedStorage/ecs/components/types.d.ts new file mode 100644 index 0000000..62d9f1c --- /dev/null +++ b/src/ReplicatedStorage/ecs/components/types.d.ts @@ -0,0 +1,72 @@ +/** + * The Model component. + * + * Provides a reference to the {@link PVInstance} that represents the attached + * entity. + */ +export interface Model { + model?: PVInstance +} + +/** + * The Transform component. + * + * Provides a reference {@link CFrame} that represents the world transform of + * the attached entity. + */ +export interface Transform { + cframe: CFrame, + _doNotReconcile?: true +} + + +/** + * The Health component. + * + * Holds health information including health and regeneration. + * Regen is health restored per second. + */ +export interface Health { + health: number, + maxHealth: number, + regeneration: number +} + +/** + * The Damage component. + * + * Holds health-reduction information. + */ +export interface Damage { + damage: number +} + +/** + * The PlayerCharacter component. + * + * Holds the Humanoid and Player. + */ +export interface PlayerCharacter { + humanoid: Humanoid, + player: Player +} + +/** + * The Lifetime component. + * + * Holds the time it was spawned at, how long it should last, and the time elapsed. + */ +export interface Lifetime { + spawnedAt: number, + length: number, + elapsed: number +} + +/** + * The input component. + * + * Holds @todo + */ +export interface Input { + test: number +} \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/index.ts b/src/ReplicatedStorage/ecs/index.ts new file mode 100644 index 0000000..7cc7dcf --- /dev/null +++ b/src/ReplicatedStorage/ecs/index.ts @@ -0,0 +1,89 @@ +import { World, AnyEntity, Loop, Debugger } from "@rbxts/matter" +import Plasma from "@rbxts/plasma" +import { Players, ReplicatedStorage, RunService, UserInputService } from "@rbxts/services" + +import { Host } from "ReplicatedStorage/hosts" +import { tags } from "./boundTags" +import { Model } from "./components" +import { start as startReplication, stop as stopReplication } from "./replication" +import { State } from "./state" +import { start as startSystems, stop as stopSystems } from "./systems" +import { start as startTags, stop as stopTags } from "./tags" + +const MAX_DISPLAY_ORDER = 2147483647 + +function authorize(player: Player): boolean { + return RunService.IsStudio() || game.CreatorId === player.UserId +} + +let connections: + | { + [index: string]: RBXScriptConnection + } + | undefined + +export function start(host: Host): [World, State] { + if (connections) throw "ECS already running." + + const state: State = new State() + const world = new World() + const debug = new Debugger(Plasma) + debug.authorize = authorize + + debug.findInstanceFromEntity = (id: AnyEntity): Instance | undefined => { + if (!world.contains(id)) return + const model = world.get(id, Model) + return model?.model + } + + const loop = new Loop(world, state, debug.getWidgets()) + startSystems(host, loop, debug) + debug.autoInitialize(loop) + + connections = loop.begin({ + default: RunService.Heartbeat, + Stepped: RunService.Stepped + }) + + if (host === Host.All || host === Host.Server) { + startTags(world, tags) + } + + if (host === Host.All || host === Host.Client) { + startReplication(world, state) + + const serverDebugger = ReplicatedStorage.FindFirstChild("MatterDebugger") + if (serverDebugger && serverDebugger.IsA("ScreenGui")) { + serverDebugger.DisplayOrder = MAX_DISPLAY_ORDER + } + + const clientDebugger = Players.LocalPlayer.FindFirstChild("MatterDebugger") + if (clientDebugger && clientDebugger.IsA("ScreenGui")) { + clientDebugger.DisplayOrder = MAX_DISPLAY_ORDER + } + + UserInputService.InputBegan.Connect((input) => { + if (input.KeyCode === Enum.KeyCode.F4 && authorize(Players.LocalPlayer)) { + debug.toggle() + state.debugEnabled = debug.enabled + } + }) + } + + return [world, state] +} + +/** + * Stops the ECS. + */ +export function stop(): void { + if (!connections) return + for (const [_, connection] of pairs(connections)) { + connection.Disconnect() + } + connections = undefined + stopTags() + stopReplication() + stopSystems() + ReplicatedStorage.FindFirstChild("MatterDebuggerRemote")?.Destroy() +} \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/replication.ts b/src/ReplicatedStorage/ecs/replication.ts new file mode 100644 index 0000000..fbe91c6 --- /dev/null +++ b/src/ReplicatedStorage/ecs/replication.ts @@ -0,0 +1,104 @@ +import { AnyEntity, World } from "@rbxts/matter" +import { waitForEvent } from "ReplicatedStorage/remotes" +import * as Components from "./components" +import { State } from "./state" + +type ComponentNames = keyof typeof Components +type ComponentConstructors = (typeof Components)[ComponentNames] + +const DEBUG_SPAWN = "Spawn %ds%d with %s" +const DEBUG_DESPAWN = "Despawn %ds%d" +const DEBUG_MODIFY = "Modify %ds%d adding %s, removing %s" + +let connection: RBXScriptConnection | undefined + +/** + * Starts the replication receiver. + * + * @param world - The world to replicate components in + * @param state - The global state for the ECS + */ +export function start(world: World, state: State): void { + if (connection) return + + function debugPrint(message: string, args: () => (string | number)[]): void { + if (state.debugEnabled) { + print("ECS Replication>", string.format(message, ...args())) + } + } + + const replicationEvent = waitForEvent("EcsReplication") + const serverToClientEntity = new Map() + + connection = replicationEvent.OnClientEvent.Connect( + (entities: Map>) => { + for (const [serverId, componentMap] of entities) { + const clientId = serverToClientEntity.get(serverId) + + if (clientId !== undefined && next(componentMap)[0] === undefined) { + world.despawn(clientId) + serverToClientEntity.delete(serverId) + debugPrint(DEBUG_DESPAWN, () => [clientId, serverId]) + continue + } + + const componentsToInsert: Components.GooplerComponent[] = [] + const componentsToRemove: ComponentConstructors[] = [] + const insertNames: ComponentNames[] = [] + const removeNames: ComponentNames[] = [] + + for (const [name, container] of componentMap) { + const component = Components[name] + if (container.data) { + componentsToInsert.push( + // The type of component above is an intersection of all possible + // component types since it can't know which specific component is + // associated with the name. Therefore here, we must cast to an + // intersection so that the data can be used. + // + // This is okay because the data must be associated with the name + // it was created with, but the type checker can't verify this for + // us. To solve this the type must somehow be associated with the + // name in the type system. For now, this cast works fine. + component(container.data as UnionToIntersection) + ) + insertNames.push(name) + } else { + componentsToRemove.push(component) + removeNames.push(name) + } + } + + if (clientId === undefined) { + const clientId = world.spawn(...componentsToInsert) + serverToClientEntity.set(serverId, clientId) + debugPrint(DEBUG_SPAWN, () => [clientId, serverId, insertNames.join(",")]) + } else { + if (componentsToInsert.size() > 0) { + world.insert(clientId, ...componentsToInsert) + } + + if (componentsToRemove.size() > 0) { + world.remove(clientId, ...componentsToRemove) + } + + debugPrint(DEBUG_MODIFY, () => [ + clientId, + serverId, + insertNames.size() > 0 ? insertNames.join(", ") : "nothing", + removeNames.size() > 0 ? removeNames.join(", ") : "nothing" + ]) + } + } + } + ) +} + +/** + * Stops receiving replication. + */ +export function stop(): void { + if (!connection) return + connection.Disconnect() + connection = undefined +} \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/state.ts b/src/ReplicatedStorage/ecs/state.ts new file mode 100644 index 0000000..29a462a --- /dev/null +++ b/src/ReplicatedStorage/ecs/state.ts @@ -0,0 +1,8 @@ +/** + * The global ECS state. + */ +export class State { + [index: string]: unknown, + // eslint-disable-next-line roblox-ts/no-private-identifier + debugEnabled = false +} \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/systems/client/test.ts b/src/ReplicatedStorage/ecs/systems/client/test.ts new file mode 100644 index 0000000..55ab8ec --- /dev/null +++ b/src/ReplicatedStorage/ecs/systems/client/test.ts @@ -0,0 +1,8 @@ +/** + * A test system that does nothing. + */ +function test(): void { + // A test system. +} + +export = test \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/systems/index.ts b/src/ReplicatedStorage/ecs/systems/index.ts new file mode 100644 index 0000000..5a64677 --- /dev/null +++ b/src/ReplicatedStorage/ecs/systems/index.ts @@ -0,0 +1,108 @@ +import { AnySystem, Debugger, Loop } from "@rbxts/matter" +import { Context, HotReloader } from "@rbxts/rewire" +import { ServerScriptService } from "@rbxts/services" +import { Host } from "ReplicatedStorage/hosts" + +const ERROR_CONTAINER = "%s container not found" + +const shared = script.FindFirstChild("shared") +const client = script.FindFirstChild("client") +const server = ServerScriptService.FindFirstChild("goopler") + ?.FindFirstChild("ecs") + ?.FindFirstChild("systems") + ?.FindFirstChild("server") + +let firstRunSystems: AnySystem[] | undefined = [] +let hotReloader: HotReloader | undefined + +/** + * Starts the system loader. + * + * Loads systems for the specified container into the provided loop and + * debugger. Systems are hot reloaded as they are changed. + * + * @param container - The container to load + * @param loop - The ECS loop to load systems into + * @param debug - The debugger to load systems into + * + * @throws "[container] container not found" + * This is thrown when a container necessary for the provided host doesn't + * exist. + */ +export function start(container: Host, loop: Loop, debug: Debugger): void { + if (!firstRunSystems) return + + const containers: Instance[] = [] + if (!shared) throw string.format(ERROR_CONTAINER, "Shared") + containers.push(shared) + + if (container === Host.All || container === Host.Client) { + if (!client) throw string.format(ERROR_CONTAINER, "Client") + containers.push(client) + } + if (container === Host.All || container === Host.Server) { + if (!server) throw string.format(ERROR_CONTAINER, "Server") + containers.push(server) + } + + const systemsByModule: Map = new Map() + + function load(module: ModuleScript, context: Context): void { + if ( + module.Name.match("%.spec$")[0] !== undefined || + module.Name.match("%.dev$")[0] !== undefined + ) + return + const original = context.originalModule + const previous = systemsByModule.get(original) + const [ok, required] = pcall(require, module) + + if (!ok) { + warn("Error when hot-reloading system", module.Name, required) + return + } + + // Here we don't know that this is necessarily a system, but we let matter + // handle this at runtime. + const system = required as AnySystem + + if (firstRunSystems) { + firstRunSystems.push(system) + } else if (previous) { + loop.replaceSystem(previous, system) + debug.replaceSystem(previous, system) + } else { + loop.scheduleSystem(system) + } + + systemsByModule.set(original, system) + } + + function unload(_: ModuleScript, context: Context): void { + if (context.isReloading) return + + const original = context.originalModule + const previous = systemsByModule.get(original) + if (previous) { + loop.evictSystem(previous) + systemsByModule.delete(original) + } + } + + hotReloader = new HotReloader() + for (const container of containers) { + hotReloader.scan(container, load, unload) + } + + loop.scheduleSystems(firstRunSystems) + firstRunSystems = undefined +} + +/** + * Stops loading systems. + */ +export function stop(): void { + if (firstRunSystems) return + firstRunSystems = [] + hotReloader?.destroy() +} \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/systems/shared/lifetimesExpire.ts b/src/ReplicatedStorage/ecs/systems/shared/lifetimesExpire.ts new file mode 100644 index 0000000..2c0d70e --- /dev/null +++ b/src/ReplicatedStorage/ecs/systems/shared/lifetimesExpire.ts @@ -0,0 +1,19 @@ +import { World, useDeltaTime } from "@rbxts/matter" +import { Lifetime } from "ReplicatedStorage/ecs/components" + +/** + * @todo + */ +function updateIdAttribute(world: World): void { + for (const [id, lifetime] of world.query(Lifetime)) { + const newLifetime = lifetime.patch({ elapsed: lifetime.elapsed + useDeltaTime() }) + + if (os.clock() > lifetime.spawnedAt + lifetime.length) { + world.despawn(id) + } else { + world.insert(id, newLifetime) + } + } +} + +export = updateIdAttribute \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/systems/shared/trackMemory.ts b/src/ReplicatedStorage/ecs/systems/shared/trackMemory.ts new file mode 100644 index 0000000..8d0c044 --- /dev/null +++ b/src/ReplicatedStorage/ecs/systems/shared/trackMemory.ts @@ -0,0 +1,25 @@ +import { DebugWidgets, World } from "@rbxts/matter" +import { Stats } from "@rbxts/services" + +const startInstanceCount = Stats.InstanceCount +const startMemory = Stats.GetTotalMemoryUsageMb() +const startTime = os.clock() + +function trackMemory(world: World, state: object, ui: DebugWidgets): void { + const currentInstanceCount = Stats.InstanceCount + const currentMemory = Stats.GetTotalMemoryUsageMb() + + ui.window("Memory Stats", () => { + ui.label(`Instances: ${currentInstanceCount}`) + + ui.label(`Gained Instances: ${currentInstanceCount - startInstanceCount}`) + + ui.label(`Memory: ${string.format("%.1f", currentMemory)}`) + + ui.label(`Gained Memory: ${string.format("%.1f", currentMemory - startMemory)}`) + + ui.label(`Time: ${string.format("%.1f", os.clock() - startTime)}`) + }) +} + +export = trackMemory \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/systems/shared/updateIdAttribute.ts b/src/ReplicatedStorage/ecs/systems/shared/updateIdAttribute.ts new file mode 100644 index 0000000..e383c46 --- /dev/null +++ b/src/ReplicatedStorage/ecs/systems/shared/updateIdAttribute.ts @@ -0,0 +1,16 @@ +import { World } from "@rbxts/matter" +import { Model } from "ReplicatedStorage/ecs/components" +import { getIdAttribute } from "ReplicatedStorage/idAttribute" + +/** + * A system that updates the ID of {@link Model | Models}. + * + * @param world - The {@link World} the system operates on + */ +function updateIdAttribute(world: World): void { + for (const [id, record] of world.queryChanged(Model)) { + record.new?.model?.SetAttribute(getIdAttribute(), id) + } +} + +export = updateIdAttribute \ No newline at end of file diff --git a/src/ReplicatedStorage/ecs/tags.ts b/src/ReplicatedStorage/ecs/tags.ts new file mode 100644 index 0000000..291b540 --- /dev/null +++ b/src/ReplicatedStorage/ecs/tags.ts @@ -0,0 +1,81 @@ +import { AnyEntity, Component, World } from "@rbxts/matter" +import { CollectionService } from "@rbxts/services" +import { getIdAttribute } from "ReplicatedStorage/idAttribute" +import { Model, Transform } from "./components" + +export type ComponentConstructor = () => Component + +const connections: RBXScriptConnection[] = [] + +/** + * Starts spawning bound tags. + * + * @param world - The world to spawn components in + * @param bound - A map of bound tags + */ +export function start( + world: World, + bound: ReadonlyMap> +): void { + function spawnBound( + instance: Instance, + component: ComponentConstructor + ): void { + let primaryPart: BasePart + if (instance.IsA("Model")) { + if (instance.PrimaryPart) { + primaryPart = instance.PrimaryPart + } else { + warn("Attempted to tag a model that has no primary part:", instance) + return + } + } else if (instance.IsA("BasePart")) { + primaryPart = instance + } else { + warn("Attempted to tag an instance that is not a Model or BasePart:", instance) + return + } + + const id = world.spawn( + component(), + Model({ + model: instance + }), + Transform({ + cframe: primaryPart.CFrame + }) + ) + instance.SetAttribute(getIdAttribute(), id) + } + + for (const [tag, component] of bound) { + for (const instance of CollectionService.GetTagged(tag)) { + spawnBound(instance, component) + } + + connections.push( + CollectionService.GetInstanceAddedSignal(tag).Connect((instance: Instance): void => { + spawnBound(instance, component) + }) + ) + + connections.push( + CollectionService.GetInstanceRemovedSignal(tag).Connect((instance: Instance): void => { + const id = instance.GetAttribute(getIdAttribute()) + if (typeIs(id, "number")) { + world.despawn(id as AnyEntity) + } + }) + ) + } +} + +/** + * Stops spawning bound tags. + */ +export function stop(): void { + for (const connection of connections) { + connection.Disconnect() + } + connections.clear() +} \ No newline at end of file diff --git a/src/ReplicatedStorage/hosts.ts b/src/ReplicatedStorage/hosts.ts new file mode 100644 index 0000000..0e29d89 --- /dev/null +++ b/src/ReplicatedStorage/hosts.ts @@ -0,0 +1,24 @@ +/** + * Represents a particular host configuration. + */ +export enum Host { + /** + * Represents properties of no particular host. + */ + None, + + /** + * Represents properties of the client. + */ + Client, + + /** + * Represents properties of the server. + */ + Server, + + /** + * Represents properties of all hosts. + */ + All +} \ No newline at end of file diff --git a/src/ReplicatedStorage/idAttribute.ts b/src/ReplicatedStorage/idAttribute.ts new file mode 100644 index 0000000..62ef51b --- /dev/null +++ b/src/ReplicatedStorage/idAttribute.ts @@ -0,0 +1,47 @@ +import { Host } from "ReplicatedStorage/hosts" + +/** + * A string that represents the default ID attribute when the environment is + * not client or server. + */ +export const unknownIdAttribute = "unknownEntityId" + +/** + * A string that represents the ID attribute when the environment is the server. + */ +export const serverIdAttribute = "serverEntityId" + +/** + * A string that represents the ID attribute when the environment is the client. + */ +export const clientIdAttribute = "clientEntityId" + +let idAttribute = unknownIdAttribute + +/** + * Gets a string that represents the current ID attribute being used. This value + * defaults to {@link unknownIdAttribute}. + * + * @return the ID attribute + */ +export function getIdAttribute(): string { + return idAttribute +} + +/** + * Sets the `idAttribute` variable based on the provided environment. + * + * @param environment - The environment to set the ID attribute for + */ +export function setEnvironment(environment: Host): void { + switch (environment) { + case Host.Server: + idAttribute = serverIdAttribute + break + case Host.Client: + idAttribute = clientIdAttribute + break + default: + idAttribute = unknownIdAttribute + } +} \ No newline at end of file diff --git a/src/ReplicatedStorage/remotes.ts b/src/ReplicatedStorage/remotes.ts new file mode 100644 index 0000000..b76cd40 --- /dev/null +++ b/src/ReplicatedStorage/remotes.ts @@ -0,0 +1,196 @@ +const ERROR_MISSING = "%s '%s' was requested, but no such %s exists." +const ERROR_MISSING_EVENT = ERROR_MISSING.format("Event", "%s", "remote event") +const ERROR_MISSING_FUNCTION = ERROR_MISSING.format("Function", "%s", "remote function") + +const ERROR_INVALID = "%s '%s' was requested, but was not a %s." +const ERROR_INVALID_EVENT = ERROR_INVALID.format("Event", "%s", "remote event") +const ERROR_INVALID_FUNCTION = ERROR_INVALID.format("Function", "%s", "remote function") + +const events = new Map() +const functions = new Map() + +/** + * Gets a remote event by name. If the event doesn't exist it is created. + * + * @param name - The name of the event + * @returns The remote event associated with the name + */ +export function getEvent(name: string): RemoteEvent { + let event = events.get(name) + if (!event) { + const instance = script.FindFirstChild(name) + if (instance && instance.IsA("RemoteEvent")) { + event = instance + } else { + event = new Instance("RemoteEvent") + event.Name = name + event.Parent = script + } + events.set(name, event) + } + return event +} + +/** + * Gets a remote event by name. + * + * @param name - The name of the event + * @returns The remote event associated with the name + * + * @throws "Event '[name]' was requested, but no such remote event exists." + * This is thrown when an event name is provided that doesn't exist. If this is + * not desired, see {@link getEvent} and {@link waitForEvent} instead. + * + * @throws "Event '[name]' was requested, but was not a remote event." + * This is thrown when an event name provided exists, but is not an event. + * Usually this is because it's a function instead. + */ +export function getEventOrFail(name: string): RemoteEvent { + let event = events.get(name) + if (!event) { + const instance = script.FindFirstChild(name) + if (!instance) { + throw ERROR_MISSING_EVENT.format(name) + } + if (!instance.IsA("RemoteEvent")) { + throw ERROR_INVALID_EVENT.format(name) + } + event = instance + events.set(name, event) + } + return event +} + +/** + * Gets a remote event by name, waiting for it if it doesn't exist. + * + * @param name - The name of the event + * @param timeout - The time to wait for the event + * @returns The remote event associated with the name + * + * @throws "Event '[name]' was requested, but was not a remote event." + * This is thrown when an event name provided exists, but is not an event. + * Usually this is because it's a function instead. + */ +export function waitForEvent(name: string): RemoteEvent +export function waitForEvent(name: string, timeout: number): RemoteEvent | undefined +export function waitForEvent(name: string, timeout?: number): RemoteEvent | undefined { + let event = events.get(name) + if (!event) { + const instance = + timeout !== undefined ? script.WaitForChild(name, timeout) : script.WaitForChild(name) + if (!instance) return + if (!instance.IsA("RemoteEvent")) { + throw ERROR_INVALID_EVENT.format(name) + } + event = instance + events.set(name, event) + } + return event +} + +/** + * Destroys the event associated with a name. + * + * @param name - The name of the event + */ +export function destroyEvent(name: string): void { + const event = events.get(name) + if (event) { + event.Destroy() + events.delete(name) + } +} + +/** + * Gets a remote function by name. If the function doesn't exist it is created. + * + * @param name - The name of the function + * @returns The remote function associated with the name + */ +export function getFunction(name: string): RemoteFunction { + let fn = functions.get(name) + if (!fn) { + const instance = script.FindFirstChild(name) + if (instance && instance.IsA("RemoteFunction")) { + fn = instance + } else { + fn = new Instance("RemoteFunction") + fn.Name = name + fn.Parent = script + } + functions.set(name, fn) + } + return fn +} + +/** + * Gets a remote function by name. + * + * @param name - The name of the function + * @returns The remote function associated with the name + * + * @throws "Function '[name]' was requested, but no such remote function exists." + * This is thrown when a function name is provided that doesn't exist. If this + * is not desired, see {@link getFunction} and {@link waitForFunction} instead. + * + * @throws "Function '[name]' was requested, but was not a remote function." + * This is thrown when a function name provided exists, but is not a function. + * Usually this is because it's an event instead. + */ +export function getFunctionOrFail(name: string): RemoteFunction { + let fn = functions.get(name) + if (!fn) { + const instance = script.FindFirstChild(name) + if (!instance) { + throw ERROR_MISSING_FUNCTION.format(name) + } + if (!instance.IsA("RemoteFunction")) { + throw ERROR_INVALID_FUNCTION.format(name) + } + fn = instance + functions.set(name, fn) + } + return fn +} + +/** + * Gets a remote function by name, waiting for it if it doesn't exist. + * + * @param name - The name of the function + * @param timeout - The time to wait for the function + * @returns The remote function associated with the name + * + * @throws "Function '[name]' was requested, but was not a remote function." + * This is thrown when a function name provided exists, but is not a function. + * Usually this is because it's an event instead. + */ +export function waitForFunction(name: string): RemoteFunction +export function waitForFunction(name: string, timeout: number): RemoteFunction | undefined +export function waitForFunction(name: string, timeout?: number): RemoteFunction | undefined { + let fn = functions.get(name) + if (!fn) { + const instance = + timeout !== undefined ? script.WaitForChild(name, timeout) : script.WaitForChild(name) + if (!instance) return + if (!instance.IsA("RemoteFunction")) { + throw ERROR_INVALID_FUNCTION.format(name) + } + fn = instance + functions.set(name, fn) + } + return fn +} + +/** + * Destroys the function associated with a name. + * + * @param name - The name of the function + */ +export function destroyFunction(name: string): void { + const fn = functions.get(name) + if (fn) { + fn.Destroy() + functions.delete(name) + } +} \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/damageHurts.ts b/src/ServerScriptService/ecs/systems/server/damageHurts.ts new file mode 100644 index 0000000..8a8cf6e --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/damageHurts.ts @@ -0,0 +1,21 @@ +import { World } from "@rbxts/matter" +import { Damage, Health } from "ReplicatedStorage/ecs/components" + +/** + * A system that reduces health when Damage components are applied. + * + * This only adjusts any attached Health component. + */ +function damageHurts(world: World): void { + for (const [id, health, damage] of world.query(Health, Damage)) { + world.insert( + id, + health.patch({ + health: health.health - damage.damage + }) + ) + world.remove(id, Damage) + } +} + +export = damageHurts \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/healthKills.ts b/src/ServerScriptService/ecs/systems/server/healthKills.ts new file mode 100644 index 0000000..c6a7dab --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/healthKills.ts @@ -0,0 +1,37 @@ +import { World } from "@rbxts/matter" +import { Health, Model } from "ReplicatedStorage/ecs/components" + +/** + * A system that mirrors health to Humanoids. + * + * If a health component has an attached model that contains a humanoid. This + * will reflect the changes in health to that humanoid. + * + * If the health is out of bounds between 0 and the max health it is clamped + * between these values. + */ +function healthKills(world: World): void { + for (const [id, record] of world.queryChanged(Health)) { + if (!record.new) continue + const health = math.clamp(record.new.health, 0, record.new.maxHealth) + + if (health !== record.new.health) { + world.insert( + id, + record.new.patch({ + health: health + }) + ) + } + + const model = world.get(id, Model) + if (!model) continue + const humanoid = model.model?.FindFirstChildOfClass("Humanoid") + if (humanoid) { + humanoid.MaxHealth = record.new.maxHealth + humanoid.Health = health + } + } +} + +export = healthKills \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/healthRegenerates.ts b/src/ServerScriptService/ecs/systems/server/healthRegenerates.ts new file mode 100644 index 0000000..d941ac0 --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/healthRegenerates.ts @@ -0,0 +1,24 @@ +import { useThrottle, World } from "@rbxts/matter" +import { Health } from "ReplicatedStorage/ecs/components" + +/** + * @todo + */ +function healthRegenerates(world: World): void { + for (const [id, health] of world.query(Health)) { + if (health.health <= 0) continue + + const newHealth = math.clamp(health.health + health.regeneration, 0, health.maxHealth) + + if (useThrottle(1)) { + world.insert( + id, + health.patch({ + health: newHealth + }) + ) + } + } +} + +export = healthRegenerates \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/playersArePlayerCharacters.ts b/src/ServerScriptService/ecs/systems/server/playersArePlayerCharacters.ts new file mode 100644 index 0000000..a100a58 --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/playersArePlayerCharacters.ts @@ -0,0 +1,33 @@ +import { Players } from "@rbxts/services" +import { World, useEvent } from "@rbxts/matter" +import { Health, Model, PlayerCharacter } from "ReplicatedStorage/ecs/components" + +/** + * @todo + */ +function playersArePlayerCharacters(world: World): void { + Players.GetPlayers().forEach((player, _) => { + for (const [_, character] of useEvent(player, "CharacterAdded")) { + world.spawn( + Model({ + model: character + }), + PlayerCharacter({ + player: Players.GetPlayerFromCharacter(character) as Player, + humanoid: character.WaitForChild("Humanoid") as Humanoid + }), + Health({ + health: 80, + maxHealth: 100, + regeneration: 0.25 + }) + ) + } + }) + + for (const [id] of world.query(PlayerCharacter).without(Model)) { + world.despawn(id) + } +} + +export = playersArePlayerCharacters \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/playersRagdollOnDeath.ts b/src/ServerScriptService/ecs/systems/server/playersRagdollOnDeath.ts new file mode 100644 index 0000000..a6df6e0 --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/playersRagdollOnDeath.ts @@ -0,0 +1,41 @@ +import { World, useEvent } from "@rbxts/matter" +import { Model, PlayerCharacter } from "ReplicatedStorage/ecs/components" + +/** + * @todo + */ +function playersRagdollOnDeath(world: World): void { + for (const [_, playerCharacter, model] of world.query(PlayerCharacter, Model)) { + if (!model.model) continue + + playerCharacter.humanoid.BreakJointsOnDeath = false + + for (const [_] of useEvent(playerCharacter.humanoid, "Died")) { + model.model.GetDescendants().forEach((v, _) => { + if (v.IsA("Motor6D")) { + const attachment0 = new Instance("Attachment") + const attachment1 = new Instance("Attachment") + attachment0.CFrame = v.C0 + attachment1.CFrame = v.C1 + attachment0.Parent = v.Part0 + attachment1.Parent = v.Part1 + + const ballSocketConstraint = new Instance("BallSocketConstraint") + ballSocketConstraint.Attachment0 = attachment0 + ballSocketConstraint.Attachment1 = attachment1 + ballSocketConstraint.LimitsEnabled = true + ballSocketConstraint.TwistLimitsEnabled = true + ballSocketConstraint.Parent = v.Parent + + v.Destroy() + } + }) + } + } + + for (const [id] of world.query(PlayerCharacter).without(Model)) { + world.despawn(id) + } +} + +export = playersRagdollOnDeath \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/removeMissingModels.ts b/src/ServerScriptService/ecs/systems/server/removeMissingModels.ts new file mode 100644 index 0000000..13114c8 --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/removeMissingModels.ts @@ -0,0 +1,30 @@ +import { useEvent, World } from "@rbxts/matter" +import { Model } from "ReplicatedStorage/ecs/components" + +/** + * A system that removes missing {@link Model | Models}. + * + * If a model is removed from the game, this system will remove the + * corresponding model from the world. + * + * If a model is removed from the world, this system will remove the + * corresponding model from the game. + */ +function removeMissingModels(world: World): void { + for (const [id, model] of world.query(Model)) { + if (!model.model) continue + for (const _ of useEvent(model.model, "AncestryChanged")) { + if (!model.model.IsDescendantOf(game)) { + world.remove(id, Model) + break + } + } + } + + for (const [_, record] of world.queryChanged(Model)) { + if (record.new) continue + record.old?.model?.Destroy() + } +} + +export = removeMissingModels \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/replication.ts b/src/ServerScriptService/ecs/systems/server/replication.ts new file mode 100644 index 0000000..61edbcf --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/replication.ts @@ -0,0 +1,83 @@ +import { useEvent, World } from "@rbxts/matter" +import { Players } from "@rbxts/services" +import * as Components from "ReplicatedStorage/ecs/components" +import { getEvent } from "ReplicatedStorage/remotes" + +type ComponentName = keyof typeof Components +type ComponentConstructor = (typeof Components)[ComponentName] + +const REPLICATED_COMPONENT_NAMES: readonly ComponentName[] = ["Model", "Health"] + +const replicatedComponents: ReadonlySet = REPLICATED_COMPONENT_NAMES.reduce( + (set: Set, name: ComponentName) => { + return set.add(Components[name]) + }, + new Set() +) + +getEvent("EcsReplication") + +function replication(world: World): void { + const replicationEvent = getEvent("EcsReplication") + + let payload: Map> | undefined + for (const [_, player] of useEvent(Players, "PlayerAdded")) { + if (!payload) { + payload = new Map() + + for (const [id, entityData] of world) { + const entityPayload: Map = + new Map() + payload.set(tostring(id), entityPayload) + + for (const [component, componentData] of entityData) { + if (replicatedComponents.has(component)) { + // Here we are certain that the component has the name of one of our + // components because it exists in our set of components. + entityPayload.set(tostring(component) as ComponentName, { data: componentData }) + } + } + } + } + + print("Sending initial payload to", player) + replicationEvent.FireClient(player, payload) + } + + const changes: Map< + string, + Map + > = new Map() + for (const component of replicatedComponents) { + // Here we are certain that the component has the name of one of our + // components since it came from our set. + const name = tostring(component) as ComponentName + + for (const [id, record] of world.queryChanged(component)) { + const key = tostring(id) + + if (!changes.has(key)) { + changes.set(key, new Map()) + } + + if (world.contains(id)) { + changes.get(key)?.set(name, { data: record.new }) + } + } + } + + if (!changes.isEmpty()) { + replicationEvent.FireAllClients(changes) + } +} + +/** + * A system that replicates all replicated components to the client. + * + * This system runs after after all other systems to ensure any changes made are + * included. + */ +export = { + system: replication, + priority: math.huge +} \ No newline at end of file diff --git a/src/ServerScriptService/ecs/systems/server/updateTransforms.ts b/src/ServerScriptService/ecs/systems/server/updateTransforms.ts new file mode 100644 index 0000000..55ad6f1 --- /dev/null +++ b/src/ServerScriptService/ecs/systems/server/updateTransforms.ts @@ -0,0 +1,71 @@ +import { World } from "@rbxts/matter" +import { Workspace } from "@rbxts/services" +import { Model, Transform } from "ReplicatedStorage/ecs/components" +import removeMissingModels from "./removeMissingModels" + +function updateTransforms(world: World): void { + for (const [id, record] of world.queryChanged(Transform)) { + if (!world.contains(id)) continue + const model = world.get(id, Model) + if (!model || !record.new || record.new._doNotReconcile) continue + model.model?.PivotTo(record.new.cframe) + } + + for (const [id, record] of world.queryChanged(Model)) { + if (!world.contains(id)) continue + const transform = world.get(id, Transform) + if (!transform) continue + record.new?.model?.PivotTo(transform.cframe) + } + + for (const [id, model, transform] of world.query(Model, Transform)) { + if (!model.model) continue + + let primaryPart: BasePart + if (model.model.IsA("Model")) { + if (!model.model.PrimaryPart) continue + primaryPart = model.model.PrimaryPart + } else if (model.model.IsA("BasePart")) { + primaryPart = model.model + } else { + continue + } + + if (primaryPart.Anchored) continue + + if (transform.cframe.Y < Workspace.FallenPartsDestroyHeight) { + world.despawn(id) + continue + } + + if (transform.cframe !== primaryPart.CFrame) { + world.insert( + id, + Transform({ + cframe: primaryPart.CFrame, + _doNotReconcile: true + }) + ) + } + } +} + +/** + * A system that updates {@link Transform | Transforms}. + * + * If a Transform is updated, the corresponding {@link Model} is updated to + * match the Transform. + * + * If a Model is updated, the new referenced instance is updated to match the + * Transform. + * + * If an non-anchored model moves, the Transform is updated to match the updated + * instance transform. + * + * This system runs after {@link removeMissingModels} to ensure updates aren't + * performed unnecessarily. + */ +export = { + system: updateTransforms, + after: [removeMissingModels] +} \ No newline at end of file diff --git a/src/ServerScriptService/main.server.ts b/src/ServerScriptService/main.server.ts new file mode 100644 index 0000000..eb3c851 --- /dev/null +++ b/src/ServerScriptService/main.server.ts @@ -0,0 +1,13 @@ +import { start } from "ReplicatedStorage/ecs" +import { Host } from "ReplicatedStorage/hosts" +import { setEnvironment } from "ReplicatedStorage/idAttribute" +import { getEvent } from "ReplicatedStorage/remotes" + +const HOST = Host.Server + +// We only do this here at the moment to create a dummy event for replication. +// In the future this will be created by the replication system. +getEvent("EcsReplication") + +setEnvironment(HOST) +start(HOST) \ No newline at end of file diff --git a/src/StarterPlayer/StarterCharacterScripts/Health.server.ts b/src/StarterPlayer/StarterCharacterScripts/Health.server.ts new file mode 100644 index 0000000..cadfccc --- /dev/null +++ b/src/StarterPlayer/StarterCharacterScripts/Health.server.ts @@ -0,0 +1,3 @@ +// This file exists to disable the roblox health regeneration +// This seems hacky to override something, but this is officially endorsed! +// See: https://create.roblox.com/docs/reference/engine/classes/Humanoid#Health \ No newline at end of file diff --git a/src/StarterPlayer/StarterPlayerScripts/main.client.ts b/src/StarterPlayer/StarterPlayerScripts/main.client.ts new file mode 100644 index 0000000..d2c9573 --- /dev/null +++ b/src/StarterPlayer/StarterPlayerScripts/main.client.ts @@ -0,0 +1,8 @@ +import { start } from "ReplicatedStorage/ecs" +import { Host } from "ReplicatedStorage/hosts" +import { setEnvironment } from "ReplicatedStorage/idAttribute" + +const HOST = Host.Client + +setEnvironment(HOST) +start(HOST) \ No newline at end of file diff --git a/tests/integration/integration.spec.ts b/tests/integration/integration.spec.ts new file mode 100644 index 0000000..17d321c --- /dev/null +++ b/tests/integration/integration.spec.ts @@ -0,0 +1,9 @@ +/// + +export = (): void => { + describe("Integration Test", () => { + it("should be true", () => { + expect(true).to.equal(true) + }) + }) +} \ No newline at end of file diff --git a/tests/runners/run.meta.json b/tests/runners/run.meta.json new file mode 100644 index 0000000..e822b17 --- /dev/null +++ b/tests/runners/run.meta.json @@ -0,0 +1,5 @@ +{ + "properties": { + "Disabled": true + } +} \ No newline at end of file diff --git a/tests/runners/run.server.ts b/tests/runners/run.server.ts new file mode 100644 index 0000000..cade725 --- /dev/null +++ b/tests/runners/run.server.ts @@ -0,0 +1,12 @@ +import { test } from "./util/tests" +import { roots } from "./util/roots" + +const [completed, result] = test(roots) + +if (completed) { + if (!result) { + error("Tests have failed.", 0) + } +} else { + error(result, 0) +} \ No newline at end of file diff --git a/tests/runners/util/rootTree.json b/tests/runners/util/rootTree.json new file mode 100644 index 0000000..9aea0a6 --- /dev/null +++ b/tests/runners/util/rootTree.json @@ -0,0 +1,14 @@ +{ + "ReplicatedStorage": { + "goopler": {}, + "tests": {} + }, + "ServerScriptService": { + "goopler": {} + }, + "StarterPlayer": { + "StarterPlayerScripts": { + "goopler": {} + } + } +} \ No newline at end of file diff --git a/tests/runners/util/roots.ts b/tests/runners/util/roots.ts new file mode 100644 index 0000000..55465ee --- /dev/null +++ b/tests/runners/util/roots.ts @@ -0,0 +1,33 @@ +import * as rootTree from "./rootTree.json" + +type Node = { [key: string]: Node } + +type RootNode = { + [key in keyof Services]?: Node; +} + +function getRoots(node: Node, parent: Instance): Instance[] { + const roots: Instance[] = [] + for (const [key, subNode] of pairs(node)) { + const subRoots = getRoots( + subNode, + parent.FindFirstChild(key) ?? error(`Could not find child ${key}`) + ) + subRoots.move(0, subRoots.size(), roots.size(), roots) + } + if (roots.isEmpty()) { + roots.push(parent) + } + return roots +} + +function getAllRoots(node: RootNode): Instance[] { + const roots: Instance[] = [] + for (const [key, subNode] of pairs(node)) { + const subRoots = getRoots(subNode, game.GetService(key)) + subRoots.move(0, subRoots.size(), roots.size(), roots) + } + return roots +} + +export const roots = getAllRoots(rootTree) \ No newline at end of file diff --git a/tests/runners/util/tests.ts b/tests/runners/util/tests.ts new file mode 100644 index 0000000..53511e4 --- /dev/null +++ b/tests/runners/util/tests.ts @@ -0,0 +1,16 @@ +import { TestBootstrap } from "@rbxts/testez" + +export function test( + roots: Instance[] +): LuaTuple<[completed: true, result: boolean] | [completed: false, error: string]> { + print() + const call = xpcall( + () => { + const results = TestBootstrap.run(roots) + return results.failureCount === 0 + }, + (err) => debug.traceback(tostring(err)) + ) + print() + return call +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..05870df --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // required + "allowSyntheticDefaultImports": true, + "downlevelIteration": true, + "jsx": "react", + "jsxFactory": "Roact.createElement", + "jsxFragmentFactory": "Roact.Fragment", + "module": "commonjs", + "moduleResolution": "Node", + "noLib": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "moduleDetection": "force", + "strict": true, + "target": "ESNext", + "typeRoots": ["node_modules/@rbxts"], + + // configurable + "rootDirs": ["src", "tests"], + "outDir": "out", + "baseUrl": "src", + "incremental": true, + "tsBuildInfoFile": "out/tsconfig.tsbuildinfo" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..a52740d --- /dev/null +++ b/yarn.lock @@ -0,0 +1,958 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.5.2" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6" + integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw== + +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@rbxts/compiler-types@^2.0.4-types.1": + version "2.0.4-types.1" + resolved "https://registry.yarnpkg.com/@rbxts/compiler-types/-/compiler-types-2.0.4-types.1.tgz#d37edf0766aa41895085c92795c6e65261355a53" + integrity sha512-sLFiyMH9j8kcCd07lt6R+WgneksCGqYUFKu7qX02ufUU7r9eR2dXoOt2A9Nxh3unTuCu5mYyO0M6JYb82fOHxA== + +"@rbxts/matter@^0.6.2-ts.6": + version "0.6.2-ts.6" + resolved "https://registry.yarnpkg.com/@rbxts/matter/-/matter-0.6.2-ts.6.tgz#e6287da3f1fb4df6befcbe3bc3605c1c66a8e8d2" + integrity sha512-M/LHRYC0QVm0klRFef97nwTBPM9Y2v1gBno2JbE75KpFEtiFhM//dz51e62NXp7Vbx9d4qnO/ldBBoc+HmeBtQ== + +"@rbxts/plasma@^0.4.1-ts.1": + version "0.4.1-ts.1" + resolved "https://registry.yarnpkg.com/@rbxts/plasma/-/plasma-0.4.1-ts.1.tgz#3d8db367c3220e6b6953cdddbf8af9f087165392" + integrity sha512-RhLkC3GQW0KeyqjFwvOUbHhsIJOHmXg+BhcKLp0IgUDVgC5GktShi3zmW6GQ319yod+RlUDG1XHjOnP3Omo4bA== + +"@rbxts/rewire@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@rbxts/rewire/-/rewire-0.3.0.tgz#47f0cd651fe405cf418936799d2a4dac6b1bb7ce" + integrity sha512-wChhGZ3kEkEsMK9ZuwKpwRsC7OGVZlvxrYMR3beFgCIPXE58JKLziBLkDACmd709XCCEmsMAqv9HMCMhSTD08Q== + +"@rbxts/services@^1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@rbxts/services/-/services-1.5.1.tgz#4536a87932f28797507ed591f0061277c52ea77f" + integrity sha512-SRtfIjga0K4YYSXRpK+eH3kcTq7ZXo9OHOt0jszaOOoEOIJloMGlyuRPqesPHyhveh2AMXAZr3TYbRMSD+u+kQ== + +"@rbxts/testez@^0.4.2-ts.0": + version "0.4.2-ts.0" + resolved "https://registry.yarnpkg.com/@rbxts/testez/-/testez-0.4.2-ts.0.tgz#4475183d317182ac7099bffee6492ffcb7bcaf0b" + integrity sha512-8Q+OG9ddTD2P3aARCuRKpPqUBvuifgSnHvQMZ6jBMqUzxhIysnb0l4c3vnnaQnvdyZ1OW9tKxcdEHMGTb67uOw== + +"@rbxts/types@^1.0.689": + version "1.0.689" + resolved "https://registry.yarnpkg.com/@rbxts/types/-/types-1.0.689.tgz#a53f657636dc5b74475749508ded1fbef3e21ccd" + integrity sha512-+5fBORSpfR1FVMXTm/wgs1aDDDZugRjx666QS+X5bJnTQD5HA77dz5X7Gsy6+2nFpnrVeR+IFUznU2OV3Ustow== + +"@types/json-schema@^7.0.9": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/node@^16.10.4": + version "16.18.36" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.36.tgz#0db5d7efc4760d36d0d1d22c85d1a53accd5dc27" + integrity sha512-8egDX8dE50XyXWH6C6PRCNkTP106DuUrvdrednFouDSmCi7IOvrqr0frznfZaHifHH/3aq/7a7v9N4wdXMqhBQ== + +"@types/semver@^7.3.12": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + +"@typescript-eslint/eslint-plugin@^5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.11.tgz#8d466aa21abea4c3f37129997b198d141f09e76f" + integrity sha512-XxuOfTkCUiOSyBWIvHlUraLw/JT/6Io1365RO6ZuI88STKMavJZPNMU0lFcUTeQXEhHiv64CbxYxBNoDVSmghg== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.59.11" + "@typescript-eslint/type-utils" "5.59.11" + "@typescript-eslint/utils" "5.59.11" + debug "^4.3.4" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@^5.0.0": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.11.tgz#9f95e09313a9f96457006b0b8ed43c2e68eb8876" + integrity sha512-GkQGV0UF/V5Ra7gZMBmiD1WrYUFOJNvCZs+XQnUyJoxmqfWMXVNyB2NVCPRKefoQcpvTv9UpJyfCvsJFs8NzzQ== + dependencies: + "@typescript-eslint/utils" "5.59.11" + +"@typescript-eslint/parser@^5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.11.tgz#af7d4b7110e3068ce0b97550736de455e4250103" + integrity sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA== + dependencies: + "@typescript-eslint/scope-manager" "5.59.11" + "@typescript-eslint/types" "5.59.11" + "@typescript-eslint/typescript-estree" "5.59.11" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.11.tgz#5d131a67a19189c42598af9fb2ea1165252001ce" + integrity sha512-dHFOsxoLFtrIcSj5h0QoBT/89hxQONwmn3FOQ0GOQcLOOXm+MIrS8zEAhs4tWl5MraxCY3ZJpaXQQdFMc2Tu+Q== + dependencies: + "@typescript-eslint/types" "5.59.11" + "@typescript-eslint/visitor-keys" "5.59.11" + +"@typescript-eslint/type-utils@5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.11.tgz#5eb67121808a84cb57d65a15f48f5bdda25f2346" + integrity sha512-LZqVY8hMiVRF2a7/swmkStMYSoXMFlzL6sXV6U/2gL5cwnLWQgLEG8tjWPpaE4rMIdZ6VKWwcffPlo1jPfk43g== + dependencies: + "@typescript-eslint/typescript-estree" "5.59.11" + "@typescript-eslint/utils" "5.59.11" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.11.tgz#1a9018fe3c565ba6969561f2a49f330cf1fe8db1" + integrity sha512-epoN6R6tkvBYSc+cllrz+c2sOFWkbisJZWkOE+y3xHtvYaOE6Wk6B8e114McRJwFRjGvYdJwLXQH5c9osME/AA== + +"@typescript-eslint/typescript-estree@5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.11.tgz#b2caaa31725e17c33970c1197bcd54e3c5f42b9f" + integrity sha512-YupOpot5hJO0maupJXixi6l5ETdrITxeo5eBOeuV7RSKgYdU3G5cxO49/9WRnJq9EMrB7AuTSLH/bqOsXi7wPA== + dependencies: + "@typescript-eslint/types" "5.59.11" + "@typescript-eslint/visitor-keys" "5.59.11" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.11.tgz#9dbff49dc80bfdd9289f9f33548f2e8db3c59ba1" + integrity sha512-didu2rHSOMUdJThLk4aZ1Or8IcO3HzCw/ZvEjTTIfjIrcdd5cvSIwwDy2AOlE7htSNp7QIZ10fLMyRCveesMLg== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.59.11" + "@typescript-eslint/types" "5.59.11" + "@typescript-eslint/typescript-estree" "5.59.11" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.59.11": + version "5.59.11" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.11.tgz#dca561ddad169dc27d62396d64f45b2d2c3ecc56" + integrity sha512-KGYniTGG3AMTuKF9QBD7EIrvufkB6O6uX3knP73xbKLMpH+QRPcgnCxjWXSHjMRuOxFLovljqQgQpR0c7GvjoA== + dependencies: + "@typescript-eslint/types" "5.59.11" + eslint-visitor-keys "^3.3.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" + integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-roblox-ts@^0.0.35: + version "0.0.35" + resolved "https://registry.yarnpkg.com/eslint-plugin-roblox-ts/-/eslint-plugin-roblox-ts-0.0.35.tgz#9b1369b9ca0a454033125c2588f575985a3d0b3d" + integrity sha512-dITctpivgGVLWljPGZBGA0pvmjqA1VzTNd1K3ExOXMk6bYxSn4FyAVkrzHA+ITvAkVJYtgRzwKYJlf1HUoszQg== + dependencies: + "@types/node" "^16.10.4" + "@typescript-eslint/experimental-utils" "^5.0.0" + typescript "^4.4.4" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" + integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== + +eslint@^8.42.0: + version "8.42.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.42.0.tgz#7bebdc3a55f9ed7167251fe7259f75219cade291" + integrity sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.42.0" + "@humanwhocodes/config-array" "^0.11.10" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.0" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + 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.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +semver@^7.3.7: + version "7.5.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb" + integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typescript@^4.4.4: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +typescript@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826" + integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==