diff --git a/.envrc b/.envrc index fda7fa8fb..37152103b 100644 --- a/.envrc +++ b/.envrc @@ -4,6 +4,7 @@ if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then fi watch_file .direnv/selected-shell +watch_file formatter.nix if [ -e .direnv/selected-shell ]; then use flake ".#$(cat .direnv/selected-shell)" diff --git a/docs/nix/flake-module.nix b/docs/nix/flake-module.nix index 89052d53f..d2d82d6f4 100644 --- a/docs/nix/flake-module.nix +++ b/docs/nix/flake-module.nix @@ -54,9 +54,11 @@ module-docs = pkgs.runCommand "rendered" { - nativeBuildInputs = [ + buildInputs = [ pkgs.python3 self'.packages.clan-cli + # TODO: see postFixup clan-cli/default.nix:L188 + self'.packages.clan-cli.propagatedBuildInputs ]; } '' diff --git a/docs/site/blog/.authors.yml b/docs/site/blog/.authors.yml index b1134a8b5..65368df44 100644 --- a/docs/site/blog/.authors.yml +++ b/docs/site/blog/.authors.yml @@ -24,3 +24,8 @@ authors: description: "Core Developer" avatar: "https://clan.lol/static/profiles/qubasa.png" url: "https://github.com/Qubasa" + BrianMcGee: + name: "Brian McGee" + description: "Contributor" + avatar: "https://avatars.githubusercontent.com/u/1173648?v=4" + url: "https://bmcgee.ie" diff --git a/docs/site/blog/posts/nixos-facter.md b/docs/site/blog/posts/nixos-facter.md new file mode 100644 index 000000000..c0b25b51a --- /dev/null +++ b/docs/site/blog/posts/nixos-facter.md @@ -0,0 +1,100 @@ +--- +title: "Introducing NixOS Facter" +description: "Declarative Hardware Configuration in NixOS" +authors: + - BrianMcGee +date: 2024-07-19 +slug: nixos-facter +--- + +If you've ever installed [NixOS], you'll be familiar with a little Perl script called [nixos-generate-config]. Unsurprisingly, it generates a couple of NixOS modules based on available hardware, mounted filesystems, configured swap, etc. + +It's a critical component of the install process, aiming to ensure you have a good starting point for your NixOS system, with necessary or recommended kernel modules, file system mounts, networking config and much more. + +As solutions go, it's a solid one. It has helped many users take their first steps into this rabbit hole we call NixOS. However, it does suffer from one fundamental limitation. + +## Static Generation + +When a user generates a `hardware-configuration.nix` with `nixos-generate-config`, it makes choices based on the current state of the world as it sees it. By its very nature, then, it cannot account for changes in NixOS over time. + +A recommended configuration option today might be different two NixOS releases from now. + +To account for this, you could always run `nixos-generate-config` again. But that requires a working system, which may have broken due to the historical choices made last time, or worst-case, requiring you to fire up the installer again. + +## A Layer of Indirection + +What if, instead of generating some Nix code, we first describe the current hardware in an intermediate format? This hardware report would be _'pure'_, devoid of any reference to NixOS, and intended as a stable, longer-term representation of the system. + +From here, we can create a series of NixOS modules designed to examine the report's contents and make the same kinds of decisions that `nixos-generate-config` does. The critical difference is that as NixOS evolves, so can these modules, and with a full hardware report available we can make more interesting config choices about things such as GPUs and other devices. + +In a perfect world, we should not need to regenerate the underlying report as long as there are no hardware changes. We can take this one step further. + +Provided that certain sensitive information, such as serial numbers and MAC addresses, is filtered out, there is no reason why these hardware reports could not be shared after they are generated for things like EC2 instance types, specific laptop models, and so on, much like [NixOS Hardware] currently shares Nix configs. + +## Introducing NixOS Facter + +Still in its early stages, [NixOS Facter] is intended to do what I've described above. + +A user can generate a JSON-based hardware report using a (eventually static) Go program: `nixos-facter -o facter.json`. From there, they can include this report in their NixOS config and make use of our [NixOS modules](https://github.com/numtide/nixos-facter-modules) as follows: + +=== "**flake.nix**" + + ```nix + { + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + nixos-facter-modules.url = "github:numtide/nixos-facter-modules"; + }; + + outputs = inputs @ { + nixpkgs, + ... + }: { + nixosConfigurations.basic = nixpkgs.lib.nixosSystem { + modules = [ + inputs.nixos-facter-modules.nixosModules.facter + { config.facter.reportPath = ./facter.json; } + # ... + ]; + }; + }; + } + ``` + +=== "**without flakes**" + + ```nix + # configuration.nix + { + imports = [ + "${(builtins.fetchTarball { + url = "https://github.com/numtide/nixos-facter-modules/"; + })}/modules/nixos/facter.nix" + ]; + + config.facter.reportPath = ./facter.json; + } + ``` + +That's it. + +> We assume that users will rely on [disko], so we have not implemented file system configuration yet (it's on the roadmap). +> In the meantime, if you don't use disko you have to specify that part of the configuration yourself or take it from `nixos-generate-config`. + + +## Early Days + +Please be aware that [NixOS Facter] is still in early development and is still subject to significant changes especially the output json format as we flesh things out. Our initial goal is to reach feature parity with [nixos-generate-config]. + +From there, we want to continue building our NixOS modules, opening things up to the community, and beginning to capture shared hardware configurations for providers such as Hetzner, etc. + +Over the coming weeks, we will also build up documentation and examples to make it easier to play with. For now, please be patient. + +> Side note: if you are wondering why the repo is in the [Numtide] org, we started partnering with Clan! Both companies are looking to make self-hosting easier and we're excited to be working together on this. Expect more tools and features to come! + +[NixOS Facter]: https://github.com/numtide/nixos-facter +[NixOS Hardware]: https://github.com/NixOS/nixos-hardware +[NixOS]: https://nixos.org "Declarative builds and deployments" +[Numtide]: https://numtide.com +[disko]: https://github.com/nix-community/disko +[nixos-generate-config]: https://github.com/NixOS/nixpkgs/blob/dac9cdf8c930c0af98a63cbfe8005546ba0125fb/nixos/modules/installer/tools/nixos-generate-config.pl diff --git a/formatter.nix b/formatter.nix index f5a64f8a6..8f6cbb285 100644 --- a/formatter.nix +++ b/formatter.nix @@ -30,8 +30,8 @@ { "pkgs/clan-vm-manager" = { extraPythonPackages = - # clan-app currently only exists on linux - self'.packages.clan-vm-manager.testDependencies ++ self'.packages.clan-cli.testDependencies; + # # clan-app currently only exists on linux + self'.packages.clan-vm-manager.testDependencies; modules = [ "clan_vm_manager" ]; }; } diff --git a/nixosModules/clanCore/vars/default.nix b/nixosModules/clanCore/vars/default.nix index db5d55c9b..396f4e531 100644 --- a/nixosModules/clanCore/vars/default.nix +++ b/nixosModules/clanCore/vars/default.nix @@ -18,7 +18,7 @@ in ./public/in_repo.nix # ./public/vm.nix ./secret/password-store.nix - ./secret/sops.nix + ./secret/sops # ./secret/vm.nix ]; options.clan.core.vars = lib.mkOption { diff --git a/nixosModules/clanCore/vars/interface.nix b/nixosModules/clanCore/vars/interface.nix index 4b42766f3..c86f6925d 100644 --- a/nixosModules/clanCore/vars/interface.nix +++ b/nixosModules/clanCore/vars/interface.nix @@ -72,7 +72,7 @@ in name of the generator ''; readOnly = true; - default = generator.name; + default = generator.config._module.args.name; }; secret = { description = '' @@ -87,7 +87,6 @@ in This will be set automatically ''; type = str; - readOnly = true; }; value = { description = '' @@ -109,32 +108,35 @@ in For example, a prompt named 'prompt1' will be available via $prompts/prompt1 ''; default = { }; - type = attrsOf (submodule { - options = options { - description = { - description = '' - The description of the prompted value - ''; - type = str; - example = "SSH private key"; + type = attrsOf ( + submodule (prompt: { + options = options { + description = { + description = '' + The description of the prompted value + ''; + type = str; + example = "SSH private key"; + default = prompt.config._module.args.name; + }; + type = { + description = '' + The input type of the prompt. + The following types are available: + - hidden: A hidden text (e.g. password) + - line: A single line of text + - multiline: A multiline text + ''; + type = enum [ + "hidden" + "line" + "multiline" + ]; + default = "line"; + }; }; - type = { - description = '' - The input type of the prompt. - The following types are available: - - hidden: A hidden text (e.g. password) - - line: A single line of text - - multiline: A multiline text - ''; - type = enum [ - "hidden" - "line" - "multiline" - ]; - default = "line"; - }; - }; - }); + }) + ); }; runtimeInputs = { description = '' diff --git a/nixosModules/clanCore/vars/public/in_repo.nix b/nixosModules/clanCore/vars/public/in_repo.nix index ba7a6aa41..3533681a2 100644 --- a/nixosModules/clanCore/vars/public/in_repo.nix +++ b/nixosModules/clanCore/vars/public/in_repo.nix @@ -5,8 +5,9 @@ { publicModule = "clan_cli.vars.public_modules.in_repo"; fileModule = file: { - path = - config.clan.core.clanDir + "/machines/${config.clan.core.machineName}/vars/${file.config.name}"; + path = lib.mkIf (file.config.secret == false) ( + config.clan.core.clanDir + "/machines/${config.clan.core.machineName}/vars/${file.config.name}" + ); }; }; } diff --git a/nixosModules/clanCore/vars/secret/password-store.nix b/nixosModules/clanCore/vars/secret/password-store.nix index 900737c76..ceab97029 100644 --- a/nixosModules/clanCore/vars/secret/password-store.nix +++ b/nixosModules/clanCore/vars/secret/password-store.nix @@ -4,7 +4,7 @@ lib.mkIf (config.clan.core.vars.settings.secretStore == "password-store") { fileModule = file: { - path = lib.mkIf file.secret "${config.clan.core.password-store.targetDirectory}/${config.clan.core.machineName}-${file.config.generatorName}-${file.config.name}"; + path = lib.mkIf file.config.secret "${config.clan.core.password-store.targetDirectory}/${config.clan.core.machineName}-${file.config.generatorName}-${file.config.name}"; }; secretUploadDirectory = lib.mkDefault "/etc/secrets"; secretModule = "clan_cli.vars.secret_modules.password_store"; diff --git a/nixosModules/clanCore/vars/secret/sops.nix b/nixosModules/clanCore/vars/secret/sops.nix deleted file mode 100644 index 3e22e8628..000000000 --- a/nixosModules/clanCore/vars/secret/sops.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - secretsDir = config.clan.core.clanDir + "/sops/secrets"; - groupsDir = config.clan.core.clanDir + "/sops/groups"; - - # My symlink is in the nixos module detected as a directory also it works in the repl. Is this because of pure evaluation? - containsSymlink = - path: - builtins.pathExists path - && (builtins.readFileType path == "directory" || builtins.readFileType path == "symlink"); - - containsMachine = - parent: name: type: - type == "directory" && containsSymlink "${parent}/${name}/machines/${config.clan.core.machineName}"; - - containsMachineOrGroups = - name: type: - (containsMachine secretsDir name type) - || lib.any ( - group: type == "directory" && containsSymlink "${secretsDir}/${name}/groups/${group}" - ) groups; - - filterDir = - filter: dir: - lib.optionalAttrs (builtins.pathExists dir) (lib.filterAttrs filter (builtins.readDir dir)); - - groups = builtins.attrNames (filterDir (containsMachine groupsDir) groupsDir); - secrets = filterDir containsMachineOrGroups secretsDir; -in -{ - config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { - # Before we generate a secret we cannot know the path yet, so we need to set it to an empty string - fileModule = file: { - path = - lib.mkIf file.secret - config.sops.secrets.${"vars-${config.clan.core.machineName}-${file.config.generatorName}-${file.config.name}"}.path - or "/no-such-path"; - }; - secretModule = "clan_cli.vars.secret_modules.sops"; - secretUploadDirectory = lib.mkDefault "/var/lib/sops-nix"; - }; - - config.sops = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { - secrets = builtins.mapAttrs (name: _: { - sopsFile = config.clan.core.clanDir + "/sops/secrets/${name}/secret"; - format = "binary"; - }) secrets; - # To get proper error messages about missing secrets we need a dummy secret file that is always present - defaultSopsFile = lib.mkIf config.sops.validateSopsFiles ( - lib.mkDefault (builtins.toString (pkgs.writeText "dummy.yaml" "")) - ); - age.keyFile = lib.mkIf (builtins.pathExists ( - config.clan.core.clanDir + "/sops/secrets/${config.clan.core.machineName}-age.key/secret" - )) (lib.mkDefault "/var/lib/sops-nix/key.txt"); - }; -} diff --git a/nixosModules/clanCore/vars/secret/sops/default.nix b/nixosModules/clanCore/vars/secret/sops/default.nix new file mode 100644 index 000000000..b1614f06e --- /dev/null +++ b/nixosModules/clanCore/vars/secret/sops/default.nix @@ -0,0 +1,49 @@ +{ + config, + lib, + pkgs, + ... +}: +let + + inherit (lib) flip; + + inherit (import ./funcs.nix { inherit lib; }) listVars; + + varsDir = config.clan.core.clanDir + "/sops/vars"; + + vars = listVars varsDir; + +in +{ + config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { + # Before we generate a secret we cannot know the path yet, so we need to set it to an empty string + fileModule = file: { + path = lib.mkIf file.config.secret ( + config.sops.secrets.${"${config.clan.core.machineName}/${file.config.generatorName}/${file.config.name}"}.path + or "/no-such-path" + ); + }; + secretModule = "clan_cli.vars.secret_modules.sops"; + secretUploadDirectory = lib.mkDefault "/var/lib/sops-nix"; + }; + + config.sops = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { + secrets = lib.listToAttrs ( + flip map vars (secret: { + name = secret.id; + value = { + sopsFile = config.clan.core.clanDir + "/sops/vars/${secret.id}/secret"; + format = "binary"; + }; + }) + ); + # To get proper error messages about missing secrets we need a dummy secret file that is always present + defaultSopsFile = lib.mkIf config.sops.validateSopsFiles ( + lib.mkDefault (builtins.toString (pkgs.writeText "dummy.yaml" "")) + ); + age.keyFile = lib.mkIf (builtins.pathExists ( + config.clan.core.clanDir + "/sops/secrets/${config.clan.core.machineName}-age.key/secret" + )) (lib.mkDefault "/var/lib/sops-nix/key.txt"); + }; +} diff --git a/nixosModules/clanCore/vars/secret/sops/eval-tests/default.nix b/nixosModules/clanCore/vars/secret/sops/eval-tests/default.nix new file mode 100644 index 000000000..d775350a0 --- /dev/null +++ b/nixosModules/clanCore/vars/secret/sops/eval-tests/default.nix @@ -0,0 +1,43 @@ +{ + lib ? import , + pkgs ? import { }, +}: +let + inherit (import ../funcs.nix { inherit lib; }) readDirNames listVars; + + noVars = pkgs.runCommand "empty-dir" { } '' + mkdir $out + ''; + + emtpyVars = pkgs.runCommand "empty-dir" { } '' + mkdir -p $out/vars + ''; + +in +{ + test_readDirNames = { + expr = readDirNames ./populated/vars; + expected = [ "my_machine" ]; + }; + + test_listSecrets = { + expr = listVars ./populated/vars; + expected = [ + { + machine = "my_machine"; + generator = "my_generator"; + name = "my_secret"; + } + ]; + }; + + test_listSecrets_no_vars = { + expr = listVars noVars; + expected = [ ]; + }; + + test_listSecrets_empty_vars = { + expr = listVars emtpyVars; + expected = [ ]; + }; +} diff --git a/nixosModules/clanCore/vars/secret/sops/eval-tests/populated/vars/my_machine/my_generator/my_secret b/nixosModules/clanCore/vars/secret/sops/eval-tests/populated/vars/my_machine/my_generator/my_secret new file mode 100644 index 000000000..e69de29bb diff --git a/nixosModules/clanCore/vars/secret/sops/funcs.nix b/nixosModules/clanCore/vars/secret/sops/funcs.nix new file mode 100644 index 000000000..b5700794f --- /dev/null +++ b/nixosModules/clanCore/vars/secret/sops/funcs.nix @@ -0,0 +1,29 @@ +{ + lib ? import , + ... +}: +let + inherit (builtins) readDir; + + inherit (lib) concatMap flip; +in +rec { + readDirNames = + dir: + if !(builtins.pathExists dir) then [ ] else lib.mapAttrsToList (name: _type: name) (readDir dir); + + listVars = + varsDir: + flip concatMap (readDirNames varsDir) ( + machine_name: + flip concatMap (readDirNames (varsDir + "/${machine_name}")) ( + generator_name: + flip map (readDirNames (varsDir + "/${machine_name}/${generator_name}")) (secret_name: { + machine = machine_name; + generator = generator_name; + name = secret_name; + id = "${machine_name}/${generator_name}/${secret_name}"; + }) + ) + ); +} diff --git a/nixosModules/clanCore/vars/settings-opts.nix b/nixosModules/clanCore/vars/settings-opts.nix index cd31adf98..fce8bc693 100644 --- a/nixosModules/clanCore/vars/settings-opts.nix +++ b/nixosModules/clanCore/vars/settings-opts.nix @@ -30,6 +30,7 @@ ''; }; + # TODO: see if this is the right approach. Maybe revert to secretPathFunction fileModule = lib.mkOption { type = lib.types.deferredModule; internal = true; diff --git a/pkgs/clan-app/clan_app/components/serializer.py b/pkgs/clan-app/clan_app/components/serializer.py deleted file mode 100644 index d3b0563e3..000000000 --- a/pkgs/clan-app/clan_app/components/serializer.py +++ /dev/null @@ -1,92 +0,0 @@ -import dataclasses -import logging -from dataclasses import fields, is_dataclass -from pathlib import Path -from types import UnionType -from typing import Any, get_args - -import gi - -gi.require_version("WebKit", "6.0") - -log = logging.getLogger(__name__) - - -def sanitize_string(s: str) -> str: - return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") - - -def dataclass_to_dict(obj: Any) -> Any: - """ - Utility function to convert dataclasses to dictionaries - It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries - - It does NOT convert member functions. - """ - if dataclasses.is_dataclass(obj): - return { - sanitize_string(k): dataclass_to_dict(v) - for k, v in dataclasses.asdict(obj).items() - } - elif isinstance(obj, list | tuple): - return [dataclass_to_dict(item) for item in obj] - elif isinstance(obj, dict): - return {sanitize_string(k): dataclass_to_dict(v) for k, v in obj.items()} - elif isinstance(obj, Path): - return str(obj) - elif isinstance(obj, str): - return sanitize_string(obj) - else: - return obj - - -def is_union_type(type_hint: type) -> bool: - return type(type_hint) is UnionType - - -def get_inner_type(type_hint: type) -> type: - if is_union_type(type_hint): - # Return the first non-None type - return next(t for t in get_args(type_hint) if t is not type(None)) - return type_hint - - -def from_dict(t: type, data: dict[str, Any] | None) -> Any: - """ - Dynamically instantiate a data class from a dictionary, handling nested data classes. - """ - if not data: - return None - - try: - # Attempt to create an instance of the data_class - field_values = {} - for field in fields(t): - field_value = data.get(field.name) - field_type = get_inner_type(field.type) - if field_value is not None: - # If the field is another dataclass, recursively instantiate it - if is_dataclass(field_type): - field_value = from_dict(field_type, field_value) - elif isinstance(field_type, Path | str) and isinstance( - field_value, str - ): - field_value = ( - Path(field_value) if field_type == Path else field_value - ) - - if ( - field.default is not dataclasses.MISSING - or field.default_factory is not dataclasses.MISSING - ): - # Field has a default value. We cannot set the value to None - if field_value is not None: - field_values[field.name] = field_value - else: - field_values[field.name] = field_value - - return t(**field_values) - - except (TypeError, ValueError) as e: - print(f"Failed to instantiate {t.__name__}: {e}") - return None diff --git a/pkgs/clan-app/default.nix b/pkgs/clan-app/default.nix index a8e6bcf97..e54cc5966 100644 --- a/pkgs/clan-app/default.nix +++ b/pkgs/clan-app/default.nix @@ -94,7 +94,14 @@ python3.pkgs.buildPythonApplication rec { # that all necessary dependencies are consistently available both # at build time and runtime, buildInputs = allPythonDeps ++ runtimeDependencies; - propagatedBuildInputs = allPythonDeps ++ runtimeDependencies; + propagatedBuildInputs = + allPythonDeps + ++ runtimeDependencies + ++ [ + + # TODO: see postFixup clan-cli/default.nix:L188 + clan-cli.propagatedBuildInputs + ]; # also re-expose dependencies so we test them in CI passthru = { diff --git a/pkgs/clan-app/flake-module.nix b/pkgs/clan-app/flake-module.nix index d7e97edb1..3cceb34a6 100644 --- a/pkgs/clan-app/flake-module.nix +++ b/pkgs/clan-app/flake-module.nix @@ -14,7 +14,7 @@ else { devShells.clan-app = pkgs.callPackage ./shell.nix { - inherit (config.packages) clan-app webview-ui; + inherit (config.packages) clan-app; inherit self'; }; packages.clan-app = pkgs.python3.pkgs.callPackage ./default.nix { diff --git a/pkgs/clan-app/shell.nix b/pkgs/clan-app/shell.nix index 7d123a9b4..36d0da3bc 100644 --- a/pkgs/clan-app/shell.nix +++ b/pkgs/clan-app/shell.nix @@ -12,7 +12,6 @@ python3, gtk4, libadwaita, - webview-ui, self', }: @@ -29,7 +28,7 @@ let ]); in mkShell { - inherit (clan-app) nativeBuildInputs; + inherit (clan-app) nativeBuildInputs propagatedBuildInputs; inputsFrom = [ self'.devShells.default ]; @@ -67,8 +66,5 @@ mkShell { export XDG_DATA_DIRS=${gtk4}/share/gsettings-schemas/gtk4-4.14.4:$XDG_DATA_DIRS export XDG_DATA_DIRS=${gsettings-desktop-schemas}/share/gsettings-schemas/gsettings-desktop-schemas-46.0:$XDG_DATA_DIRS - # Add the webview-ui to the .webui directory - ln -nsf ${webview-ui}/lib/node_modules/@clan/webview-ui/dist/ ./clan_app/.webui - ''; } diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 29acb1e59..e155a04d9 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -7,10 +7,10 @@ from types import ModuleType # These imports are unused, but necessary for @API.register to run once. from clan_cli.api import directory, mdns_discovery, modules from clan_cli.arg_actions import AppendOptionAction -from clan_cli.clan import show +from clan_cli.clan import show, update # API endpoints that are not used in the cli. -__all__ = ["directory", "mdns_discovery", "modules"] +__all__ = ["directory", "mdns_discovery", "modules", "update"] from . import ( backups, diff --git a/pkgs/clan-cli/clan_cli/api/__init__.py b/pkgs/clan-cli/clan_cli/api/__init__.py index 5237bc2a5..192bd4524 100644 --- a/pkgs/clan-cli/clan_cli/api/__init__.py +++ b/pkgs/clan-cli/clan_cli/api/__init__.py @@ -1,141 +1,22 @@ -import dataclasses -import json from collections.abc import Callable -from dataclasses import dataclass, fields, is_dataclass +from dataclasses import dataclass from functools import wraps from inspect import Parameter, Signature, signature -from pathlib import Path -from types import UnionType from typing import ( Annotated, Any, Generic, Literal, TypeVar, - get_args, - get_origin, get_type_hints, ) +from .serde import dataclass_to_dict, from_dict, sanitize_string + +__all__ = ["from_dict", "dataclass_to_dict", "sanitize_string"] + from clan_cli.errors import ClanError - -def sanitize_string(s: str) -> str: - # Using the native string sanitizer to handle all edge cases - # Remove the outer quotes '"string"' - return json.dumps(s)[1:-1] - - -def dataclass_to_dict(obj: Any) -> Any: - """ - Utility function to convert dataclasses to dictionaries - It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries - - It does NOT convert member functions. - """ - if is_dataclass(obj): - return { - # Use either the original name or name - sanitize_string( - field.metadata.get("original_name", field.name) - ): dataclass_to_dict(getattr(obj, field.name)) - for field in fields(obj) # type: ignore - } - elif isinstance(obj, list | tuple): - return [dataclass_to_dict(item) for item in obj] - elif isinstance(obj, dict): - return {sanitize_string(k): dataclass_to_dict(v) for k, v in obj.items()} - elif isinstance(obj, Path): - return sanitize_string(str(obj)) - elif isinstance(obj, str): - return sanitize_string(obj) - else: - return obj - - -def is_union_type(type_hint: type) -> bool: - return type(type_hint) is UnionType - - -def get_inner_type(type_hint: type) -> type: - if is_union_type(type_hint): - # Return the first non-None type - return next(t for t in get_args(type_hint) if t is not type(None)) - return type_hint - - -def get_second_type(type_hint: type[dict]) -> type: - """ - Get the value type of a dictionary type hint - """ - args = get_args(type_hint) - if len(args) == 2: - # Return the second argument, which should be the value type (Machine) - return args[1] - - raise ValueError(f"Invalid type hint for dict: {type_hint}") - - -def from_dict(t: type, data: dict[str, Any] | None) -> Any: - """ - Dynamically instantiate a data class from a dictionary, handling nested data classes. - """ - if data is None: - return None - - try: - # Attempt to create an instance of the data_class - field_values = {} - for field in fields(t): - original_name = field.metadata.get("original_name", field.name) - - field_value = data.get(original_name) - - field_type = get_inner_type(field.type) # type: ignore - - if original_name in data: - # If the field is another dataclass, recursively instantiate it - if is_dataclass(field_type): - field_value = from_dict(field_type, field_value) - elif isinstance(field_type, Path | str) and isinstance( - field_value, str - ): - field_value = ( - Path(field_value) if field_type == Path else field_value - ) - elif get_origin(field_type) is dict and isinstance(field_value, dict): - # The field is a dictionary with a specific type - inner_type = get_second_type(field_type) - field_value = { - k: from_dict(inner_type, v) for k, v in field_value.items() - } - elif get_origin is list and isinstance(field_value, list): - # The field is a list with a specific type - inner_type = get_args(field_type)[0] - field_value = [from_dict(inner_type, v) for v in field_value] - - # Set the value - if ( - field.default is not dataclasses.MISSING - or field.default_factory is not dataclasses.MISSING - ): - # Fields with default value - # a: Int = 1 - # b: list = Field(default_factory=list) - if original_name in data or field_value is not None: - field_values[field.name] = field_value - else: - # Fields without default value - # a: Int - field_values[field.name] = field_value - - return t(**field_values) - - except (TypeError, ValueError) as e: - print(f"Failed to instantiate {t.__name__}: {e} {data}") - return None - - T = TypeVar("T") ResponseDataType = TypeVar("ResponseDataType") diff --git a/pkgs/clan-cli/clan_cli/api/serde.py b/pkgs/clan-cli/clan_cli/api/serde.py new file mode 100644 index 000000000..57345c3db --- /dev/null +++ b/pkgs/clan-cli/clan_cli/api/serde.py @@ -0,0 +1,106 @@ +""" +This module provides utility functions for serialization and deserialization of data classes. + +Functions: +- sanitize_string(s: str) -> str: Ensures a string is properly escaped for json serializing. +- dataclass_to_dict(obj: Any) -> Any: Converts a data class and its nested data classes, lists, tuples, and dictionaries to dictionaries. +- from_dict(t: type[T], data: Any) -> T: Dynamically instantiates a data class from a dictionary, constructing nested data classes, validates all required fields exist and have the expected type. + +Classes: +- TypeAdapter: A Pydantic type adapter for data classes. + +Exceptions: +- ValidationError: Raised when there is a validation error during deserialization. +- ClanError: Raised when there is an error during serialization or deserialization. + +Dependencies: +- dataclasses: Provides the @dataclass decorator and related functions for creating data classes. +- json: Provides functions for working with JSON data. +- collections.abc: Provides abstract base classes for collections. +- functools: Provides functions for working with higher-order functions and decorators. +- inspect: Provides functions for inspecting live objects. +- operator: Provides functions for working with operators. +- pathlib: Provides classes for working with filesystem paths. +- types: Provides functions for working with types. +- typing: Provides support for type hints. +- pydantic: A library for data validation and settings management. +- pydantic_core: Core functionality for Pydantic. + +Note: This module assumes the presence of other modules and classes such as `ClanError` and `ErrorDetails` from the `clan_cli.errors` module. +""" + +import json +from dataclasses import dataclass, fields, is_dataclass +from pathlib import Path +from typing import ( + Any, + TypeVar, +) + +from pydantic import TypeAdapter, ValidationError +from pydantic_core import ErrorDetails + +from clan_cli.errors import ClanError + + +def sanitize_string(s: str) -> str: + # Using the native string sanitizer to handle all edge cases + # Remove the outer quotes '"string"' + return json.dumps(s)[1:-1] + + +def dataclass_to_dict(obj: Any, *, use_alias: bool = True) -> Any: + def _to_dict(obj: Any) -> Any: + """ + Utility function to convert dataclasses to dictionaries + It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries + + It does NOT convert member functions. + """ + if is_dataclass(obj): + return { + # Use either the original name or name + sanitize_string( + field.metadata.get("alias", field.name) if use_alias else field.name + ): _to_dict(getattr(obj, field.name)) + for field in fields(obj) + if not field.name.startswith("_") # type: ignore + } + elif isinstance(obj, list | tuple): + return [_to_dict(item) for item in obj] + elif isinstance(obj, dict): + return {sanitize_string(k): _to_dict(v) for k, v in obj.items()} + elif isinstance(obj, Path): + return sanitize_string(str(obj)) + elif isinstance(obj, str): + return sanitize_string(obj) + else: + return obj + + return _to_dict(obj) + + +T = TypeVar("T", bound=dataclass) # type: ignore + + +def from_dict(t: type[T], data: Any) -> T: + """ + Dynamically instantiate a data class from a dictionary, handling nested data classes. + We use dataclasses. But the deserialization logic of pydantic takes a lot of complexity. + """ + adapter = TypeAdapter(t) + try: + return adapter.validate_python( + data, + ) + except ValidationError as e: + fst_error: ErrorDetails = e.errors()[0] + if not fst_error: + raise ClanError(msg=str(e)) + + msg = fst_error.get("msg") + loc = fst_error.get("loc") + field_path = "Unknown" + if loc: + field_path = str(loc) + raise ClanError(msg=msg, location=f"{t!s}: {field_path}", description=str(e)) diff --git a/pkgs/clan-cli/clan_cli/api/util.py b/pkgs/clan-cli/clan_cli/api/util.py index 505fab545..2dd4e03c1 100644 --- a/pkgs/clan-cli/clan_cli/api/util.py +++ b/pkgs/clan-cli/clan_cli/api/util.py @@ -74,7 +74,9 @@ def type_to_dict(t: Any, scope: str = "", type_map: dict[TypeVar, type] = {}) -> if dataclasses.is_dataclass(t): fields = dataclasses.fields(t) properties = { - f.name: type_to_dict(f.type, f"{scope} {t.__name__}.{f.name}", type_map) + f.metadata.get("alias", f.name): type_to_dict( + f.type, f"{scope} {t.__name__}.{f.name}", type_map + ) for f in fields if not f.name.startswith("_") } diff --git a/pkgs/clan-cli/clan_cli/clan/inspect.py b/pkgs/clan-cli/clan_cli/clan/inspect.py index c64009460..0c5b28636 100644 --- a/pkgs/clan-cli/clan_cli/clan/inspect.py +++ b/pkgs/clan-cli/clan_cli/clan/inspect.py @@ -14,7 +14,7 @@ from ..vms.inspect import VmConfig, inspect_vm @dataclass class FlakeConfig: - flake_url: str | Path + flake_url: FlakeId flake_attr: str clan_name: str @@ -89,7 +89,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig: meta = nix_metadata(flake_url) return FlakeConfig( vm=vm, - flake_url=flake_url, + flake_url=FlakeId(flake_url), clan_name=clan_name, flake_attr=machine_name, nar_hash=meta["locked"]["narHash"], diff --git a/pkgs/clan-cli/clan_cli/history/add.py b/pkgs/clan-cli/clan_cli/history/add.py index b8c6dc011..e13938e86 100644 --- a/pkgs/clan-cli/clan_cli/history/add.py +++ b/pkgs/clan-cli/clan_cli/history/add.py @@ -62,7 +62,7 @@ def list_history() -> list[HistoryEntry]: def new_history_entry(url: str, machine: str) -> HistoryEntry: flake = inspect_flake(url, machine) - flake.flake_url = str(flake.flake_url) + flake.flake_url = flake.flake_url return HistoryEntry( flake=flake, last_used=datetime.datetime.now().isoformat(), diff --git a/pkgs/clan-cli/clan_cli/history/update.py b/pkgs/clan-cli/clan_cli/history/update.py index 6c263f80f..ed9c87abc 100644 --- a/pkgs/clan-cli/clan_cli/history/update.py +++ b/pkgs/clan-cli/clan_cli/history/update.py @@ -16,7 +16,7 @@ def update_history() -> list[HistoryEntry]: for entry in logs: try: - meta = nix_metadata(entry.flake.flake_url) + meta = nix_metadata(str(entry.flake.flake_url)) except ClanCmdError as e: print(f"Failed to update {entry.flake.flake_url}: {e}") continue @@ -31,7 +31,7 @@ def update_history() -> list[HistoryEntry]: machine_name=entry.flake.flake_attr, ) flake = inspect_flake(uri.get_url(), uri.machine_name) - flake.flake_url = str(flake.flake_url) + flake.flake_url = flake.flake_url entry = HistoryEntry( flake=flake, last_used=datetime.datetime.now().isoformat() ) diff --git a/pkgs/clan-cli/clan_cli/inventory/classes.py b/pkgs/clan-cli/clan_cli/inventory/classes.py index 41d116831..60c9107b9 100644 --- a/pkgs/clan-cli/clan_cli/inventory/classes.py +++ b/pkgs/clan-cli/clan_cli/inventory/classes.py @@ -153,7 +153,7 @@ class ServiceSingleDisk: class Service: borgbackup: dict[str, ServiceBorgbackup] = field(default_factory = dict) packages: dict[str, ServicePackage] = field(default_factory = dict) - single_disk: dict[str, ServiceSingleDisk] = field(default_factory = dict, metadata = {"original_name": "single-disk"}) + single_disk: dict[str, ServiceSingleDisk] = field(default_factory = dict, metadata = {"alias": "single-disk"}) @dataclass diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index cd6910cd1..c49377070 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -13,6 +13,7 @@ from ..facts.upload import upload_secrets from ..machines.machines import Machine from ..nix import nix_command, nix_metadata from ..ssh import HostKeyCheck +from ..vars.generate import generate_vars from .inventory import get_all_machines, get_selected_machines from .machine_group import MachineGroup @@ -93,6 +94,7 @@ def deploy_machine(machines: MachineGroup) -> None: env["NIX_SSHOPTS"] = ssh_arg generate_facts([machine], None, False) + generate_vars([machine], None, False) upload_secrets(machine) path = upload_sources(".", target) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 8a29c9a0d..516bd4cb4 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -17,6 +17,8 @@ setuptools, stdenv, + pydantic, + # custom args clan-core-path, nixpkgs, @@ -28,6 +30,7 @@ let pythonDependencies = [ argcomplete # Enables shell completions + pydantic # Dataclass deserialisation / validation / schemas ]; # load nixpkgs runtime dependencies from a json file @@ -181,6 +184,7 @@ python3.pkgs.buildPythonApplication { ''; # Clean up after the package to avoid leaking python packages into a devshell + # TODO: factor seperate cli / API packages postFixup = '' rm $out/nix-support/propagated-build-inputs ''; diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index c783a494f..deee72d8c 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -62,7 +62,12 @@ name = "clan-cli-docs"; src = ./.; - buildInputs = [ pkgs.python3 ]; + buildInputs = [ + + # TODO: see postFixup clan-cli/default.nix:L188 + pkgs.python3 + self'.packages.clan-cli.propagatedBuildInputs + ]; installPhase = '' ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py @@ -77,7 +82,12 @@ name = "clan-ts-api"; src = ./.; - buildInputs = [ pkgs.python3 ]; + buildInputs = [ + pkgs.python3 + + # TODO: see postFixup clan-cli/default.nix:L188 + self'.packages.clan-cli.propagatedBuildInputs + ]; installPhase = '' ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py diff --git a/pkgs/clan-cli/tests/test_deserializers.py b/pkgs/clan-cli/tests/test_deserializers.py new file mode 100644 index 000000000..f87b65adc --- /dev/null +++ b/pkgs/clan-cli/tests/test_deserializers.py @@ -0,0 +1,179 @@ +from dataclasses import dataclass, field +from pathlib import Path + +import pytest + +# Functions to test +from clan_cli.api import dataclass_to_dict, from_dict +from clan_cli.errors import ClanError +from clan_cli.inventory import ( + Inventory, + Machine, + MachineDeploy, + Meta, + Service, + ServiceBorgbackup, + ServiceBorgbackupRole, + ServiceBorgbackupRoleClient, + ServiceBorgbackupRoleServer, + ServiceMeta, +) + + +def test_simple() -> None: + @dataclass + class Person: + name: str + + person_dict = { + "name": "John", + } + + expected_person = Person( + name="John", + ) + + assert from_dict(Person, person_dict) == expected_person + + +def test_nested() -> None: + @dataclass + class Age: + value: str + + @dataclass + class Person: + name: str + # deeply nested dataclasses + age: Age + age_list: list[Age] + age_dict: dict[str, Age] + # Optional field + home: Path | None + + person_dict = { + "name": "John", + "age": { + "value": "99", + }, + "age_list": [{"value": "66"}, {"value": "77"}], + "age_dict": {"now": {"value": "55"}, "max": {"value": "100"}}, + "home": "/home", + } + + expected_person = Person( + name="John", + age=Age("99"), + age_list=[Age("66"), Age("77")], + age_dict={"now": Age("55"), "max": Age("100")}, + home=Path("/home"), + ) + + assert from_dict(Person, person_dict) == expected_person + + +def test_simple_field_missing() -> None: + @dataclass + class Person: + name: str + + person_dict = {} + + with pytest.raises(ClanError): + from_dict(Person, person_dict) + + +def test_deserialize_extensive_inventory() -> None: + # TODO: Make this an abstract test, so it doesn't break the test if the inventory changes + data = { + "meta": {"name": "superclan", "description": "nice clan"}, + "services": { + "borgbackup": { + "instance1": { + "meta": { + "name": "borg1", + }, + "roles": { + "client": {}, + "server": {}, + }, + } + }, + }, + "machines": {"foo": {"name": "foo", "deploy": {}}}, + } + expected = Inventory( + meta=Meta(name="superclan", description="nice clan"), + services=Service( + borgbackup={ + "instance1": ServiceBorgbackup( + meta=ServiceMeta(name="borg1"), + roles=ServiceBorgbackupRole( + client=ServiceBorgbackupRoleClient(), + server=ServiceBorgbackupRoleServer(), + ), + ) + } + ), + machines={"foo": Machine(deploy=MachineDeploy(), name="foo")}, + ) + assert from_dict(Inventory, data) == expected + + +def test_alias_field() -> None: + @dataclass + class Person: + name: str = field(metadata={"alias": "--user-name--"}) + + data = {"--user-name--": "John"} + expected = Person(name="John") + + person = from_dict(Person, data) + + # Deserialize + assert person == expected + + # Serialize with alias + assert dataclass_to_dict(person) == data + + # Serialize without alias + assert dataclass_to_dict(person, use_alias=False) == {"name": "John"} + + +def test_alias_field_from_orig_name() -> None: + """ + Field declares an alias. But the data is provided with the field name. + """ + + @dataclass + class Person: + name: str = field(metadata={"alias": "--user-name--"}) + + data = {"user": "John"} + + with pytest.raises(ClanError): + from_dict(Person, data) + + +def test_path_field() -> None: + @dataclass + class Person: + name: Path + + data = {"name": "John"} + expected = Person(name=Path("John")) + + assert from_dict(Person, data) == expected + + +def test_private_public_fields() -> None: + @dataclass + class Person: + name: Path + _name: str | None = None + + data = {"name": "John"} + expected = Person(name=Path("John")) + assert from_dict(Person, data) == expected + + assert dataclass_to_dict(expected) == data diff --git a/pkgs/clan-cli/tests/test_history_cli.py b/pkgs/clan-cli/tests/test_history_cli.py index d08b1bb62..e65c7b2af 100644 --- a/pkgs/clan-cli/tests/test_history_cli.py +++ b/pkgs/clan-cli/tests/test_history_cli.py @@ -27,7 +27,7 @@ def test_history_add( history_file = user_history_file() assert history_file.exists() history = [HistoryEntry(**entry) for entry in json.loads(open(history_file).read())] - assert history[0].flake.flake_url == str(test_flake_with_core.path) + assert str(history[0].flake.flake_url["loc"]) == str(test_flake_with_core.path) @pytest.mark.impure diff --git a/pkgs/clan-cli/tests/test_serializers.py b/pkgs/clan-cli/tests/test_serializers.py new file mode 100644 index 000000000..fa6557d90 --- /dev/null +++ b/pkgs/clan-cli/tests/test_serializers.py @@ -0,0 +1,106 @@ +from dataclasses import dataclass, field + +# Functions to test +from clan_cli.api import ( + dataclass_to_dict, + sanitize_string, +) + + +# +def test_sanitize_string() -> None: + # Simple strings + assert sanitize_string("Hello World") == "Hello World" + assert sanitize_string("Hello\nWorld") == "Hello\\nWorld" + assert sanitize_string("Hello\tWorld") == "Hello\\tWorld" + assert sanitize_string("Hello\rWorld") == "Hello\\rWorld" + assert sanitize_string("Hello\fWorld") == "Hello\\fWorld" + assert sanitize_string("Hello\vWorld") == "Hello\\u000bWorld" + assert sanitize_string("Hello\bWorld") == "Hello\\bWorld" + assert sanitize_string("Hello\\World") == "Hello\\\\World" + assert sanitize_string('Hello"World') == 'Hello\\"World' + assert sanitize_string("Hello'World") == "Hello'World" + assert sanitize_string("Hello\0World") == "Hello\\u0000World" + # Console escape characters + + assert sanitize_string("\033[1mBold\033[0m") == "\\u001b[1mBold\\u001b[0m" # Red + assert sanitize_string("\033[31mRed\033[0m") == "\\u001b[31mRed\\u001b[0m" # Blue + assert ( + sanitize_string("\033[42mGreen\033[0m") == "\\u001b[42mGreen\\u001b[0m" + ) # Green + assert sanitize_string("\033[4mUnderline\033[0m") == "\\u001b[4mUnderline\\u001b[0m" + assert ( + sanitize_string("\033[91m\033[1mBold Red\033[0m") + == "\\u001b[91m\\u001b[1mBold Red\\u001b[0m" + ) + + +def test_dataclass_to_dict() -> None: + @dataclass + class Person: + name: str + age: int + + person = Person(name="John", age=25) + expected_dict = {"name": "John", "age": 25} + assert dataclass_to_dict(person) == expected_dict + + +def test_dataclass_to_dict_nested() -> None: + @dataclass + class Address: + city: str = "afghanistan" + zip: str = "01234" + + @dataclass + class Person: + name: str + age: int + address: Address = field(default_factory=Address) + + person1 = Person(name="John", age=25) + expected_dict1 = { + "name": "John", + "age": 25, + "address": {"city": "afghanistan", "zip": "01234"}, + } + # address must be constructed with default values if not passed + assert dataclass_to_dict(person1) == expected_dict1 + + person2 = Person(name="John", age=25, address=Address(zip="0", city="Anywhere")) + expected_dict2 = { + "name": "John", + "age": 25, + "address": {"zip": "0", "city": "Anywhere"}, + } + assert dataclass_to_dict(person2) == expected_dict2 + + +def test_dataclass_to_dict_defaults() -> None: + @dataclass + class Foo: + home: dict[str, str] = field(default_factory=dict) + work: list[str] = field(default_factory=list) + + @dataclass + class Person: + name: str = field(default="jon") + age: int = field(default=1) + foo: Foo = field(default_factory=Foo) + + default_person = Person() + expected_default = { + "name": "jon", + "age": 1, + "foo": {"home": {}, "work": []}, + } + # address must be constructed with default values if not passed + assert dataclass_to_dict(default_person) == expected_default + + real_person = Person(name="John", age=25, foo=Foo(home={"a": "b"}, work=["a", "b"])) + expected = { + "name": "John", + "age": 25, + "foo": {"home": {"a": "b"}, "work": ["a", "b"]}, + } + assert dataclass_to_dict(real_person) == expected diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py index d88ae7c27..746361462 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py @@ -163,12 +163,12 @@ class ClanStore: del self.clan_store[str(vm.data.flake.flake_url)][vm.data.flake.flake_attr] def get_vm(self, uri: ClanURI) -> None | VMObject: - flake_id = Machine(uri.machine_name, uri.flake).get_id() - vm_store = self.clan_store.get(flake_id) + machine = Machine(uri.machine_name, uri.flake) + vm_store = self.clan_store.get(str(machine.flake)) if vm_store is None: return None - machine = vm_store.get(uri.machine_name, None) - return machine + vm = vm_store.get(str(machine.name), None) + return vm def get_running_vms(self) -> list[VMObject]: return [ diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index 172fa0666..4ff1b90d0 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -39,7 +39,7 @@ let libadwaita webkitgtk_6_0 adwaita-icon-theme - ]; + ] ++ clan-cli.propagatedBuildInputs; # Deps including python packages from the local project allPythonDeps = [ (python3.pkgs.toPythonModule clan-cli) ] ++ externalPythonDeps; @@ -84,7 +84,6 @@ python3.pkgs.buildPythonApplication rec { setuptools copyDesktopItems wrapGAppsHook - gobject-introspection ]; @@ -93,7 +92,7 @@ python3.pkgs.buildPythonApplication rec { # that all necessary dependencies are consistently available both # at build time and runtime, buildInputs = allPythonDeps ++ runtimeDependencies; - propagatedBuildInputs = allPythonDeps ++ runtimeDependencies; + propagatedBuildInputs = allPythonDeps ++ runtimeDependencies ++ [ ]; # also re-expose dependencies so we test them in CI passthru = { diff --git a/pkgs/classgen/main.py b/pkgs/classgen/main.py index a4537c276..8cafe0b6c 100644 --- a/pkgs/classgen/main.py +++ b/pkgs/classgen/main.py @@ -245,7 +245,7 @@ def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) -> field_meta = None if field_name != prop: - field_meta = f"""{{"original_name": "{prop}"}}""" + field_meta = f"""{{"alias": "{prop}"}}""" finalize_field = partial(get_field_def, field_name, field_meta) diff --git a/pkgs/webview-ui/app/eslint.config.mjs b/pkgs/webview-ui/app/eslint.config.mjs index 8a408bee5..619db917b 100644 --- a/pkgs/webview-ui/app/eslint.config.mjs +++ b/pkgs/webview-ui/app/eslint.config.mjs @@ -1,9 +1,11 @@ import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; import tailwind from "eslint-plugin-tailwindcss"; +import pluginQuery from "@tanstack/eslint-plugin-query"; export default tseslint.config( eslint.configs.recommended, + ...pluginQuery.configs["flat/recommended"], ...tseslint.configs.strict, ...tseslint.configs.stylistic, ...tailwind.configs["flat/recommended"], diff --git a/pkgs/webview-ui/app/package-lock.json b/pkgs/webview-ui/app/package-lock.json index 595f4b1cb..5f435d99c 100644 --- a/pkgs/webview-ui/app/package-lock.json +++ b/pkgs/webview-ui/app/package-lock.json @@ -9,8 +9,10 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@floating-ui/dom": "^1.6.8", "@modular-forms/solid": "^0.21.0", "@solid-primitives/storage": "^3.7.1", + "@tanstack/eslint-plugin-query": "^5.51.12", "@tanstack/solid-query": "^5.51.2", "material-icons": "^1.13.12", "nanoid": "^5.0.7", @@ -787,7 +789,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -802,7 +803,6 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -811,7 +811,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -834,7 +833,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -844,7 +842,6 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -859,7 +856,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -876,11 +872,32 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.5.tgz", + "integrity": "sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==", + "dependencies": { + "@floating-ui/utils": "^0.2.5" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz", + "integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.5" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz", + "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -894,7 +911,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -904,7 +920,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -916,7 +931,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -928,8 +942,7 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -1023,7 +1036,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1036,7 +1048,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -1045,7 +1056,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1569,6 +1579,124 @@ "node": ">=4" } }, + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.51.12", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.51.12.tgz", + "integrity": "sha512-vzUXXIVzDP2c6wVSJ+1imPGaKQ2ILuWnta64FJc/JnQ5WunfO17bQJSk6uKDbzTQG/YKgPYBMG3C9qFA4b7Ayg==", + "dependencies": { + "@typescript-eslint/utils": "8.0.0-alpha.30" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8 || ^9" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.30.tgz", + "integrity": "sha512-FGW/iPWGyPFamAVZ60oCAthMqQrqafUGebF8UKuq/ha+e9SVG6YhJoRzurlQXOVf8dHfOhJ0ADMXyFnMc53clg==", + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.0-alpha.30.tgz", + "integrity": "sha512-4WzLlw27SO9pK9UFj/Hu7WGo8WveT0SEiIpFVsV2WwtQmLps6kouwtVCB8GJPZKJyurhZhcqCoQVQFmpv441Vg==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.30.tgz", + "integrity": "sha512-WSXbc9ZcXI+7yC+6q95u77i8FXz6HOLsw3ST+vMUlFy1lFbXyFL/3e6HDKQCm2Clt0krnoCPiTGvIn+GkYPn4Q==", + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0-alpha.30.tgz", + "integrity": "sha512-rfhqfLqFyXhHNDwMnHiVGxl/Z2q/3guQ1jLlGQ0hi9Rb7inmwz42crM+NnLPR+2vEnwyw1P/g7fnQgQ3qvFx4g==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.0.0-alpha.30", + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.30" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.30.tgz", + "integrity": "sha512-XZuNurZxBqmr6ZIRIwWFq7j5RZd6ZlkId/HZEWyfciK+CWoyOxSF9Pv2VXH9Rlu2ZG2PfbhLz2Veszl4Pfn7yA==", + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tanstack/query-core": { "version": "5.51.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.1.tgz", @@ -1849,8 +1977,7 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitest/expect": { "version": "1.6.0", @@ -1952,7 +2079,6 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -1964,7 +2090,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1994,7 +2119,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2058,14 +2182,12 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -2165,8 +2287,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -2184,7 +2305,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2193,7 +2313,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -2252,7 +2371,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -2411,8 +2529,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/confbox": { "version": "0.1.7", @@ -2452,7 +2569,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2558,7 +2674,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2592,8 +2707,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -2623,7 +2737,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -2641,7 +2754,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -2753,7 +2865,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2824,7 +2935,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2840,7 +2950,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2852,7 +2961,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2861,7 +2969,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2870,7 +2977,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2885,7 +2991,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2895,7 +3000,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2911,7 +3015,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2922,14 +3025,12 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2941,7 +3042,6 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -2956,7 +3056,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2965,7 +3064,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2977,7 +3075,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2989,7 +3086,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3001,7 +3097,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -3031,7 +3126,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -3043,7 +3137,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3055,7 +3148,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -3073,7 +3165,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3104,14 +3195,12 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3127,7 +3216,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3138,14 +3226,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastparse": { "version": "1.1.2", @@ -3157,7 +3243,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -3166,7 +3251,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -3178,7 +3262,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3190,7 +3273,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3206,7 +3288,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -3219,8 +3300,7 @@ "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "node_modules/foreground-child": { "version": "3.1.1", @@ -3274,8 +3354,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -3356,7 +3435,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -3377,7 +3455,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -3404,8 +3481,7 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/has-flag": { "version": "3.0.0", @@ -3497,7 +3573,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, "engines": { "node": ">= 4" } @@ -3528,7 +3603,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3544,7 +3618,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -3554,7 +3627,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3563,8 +3635,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -3594,7 +3665,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3612,7 +3682,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3624,7 +3693,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -3633,7 +3701,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3671,8 +3738,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jackspeak": { "version": "2.3.6", @@ -3711,7 +3777,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -3774,8 +3839,7 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-schema-faker": { "version": "0.5.6", @@ -3840,14 +3904,12 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.3", @@ -3874,7 +3936,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -3911,7 +3972,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -3955,7 +4015,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -3981,8 +4040,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/loupe": { "version": "2.3.7", @@ -4078,7 +4136,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -4087,7 +4144,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -4147,7 +4203,6 @@ "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4182,8 +4237,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -4216,8 +4270,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/needle": { "version": "3.3.1", @@ -4316,7 +4369,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4349,7 +4401,6 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4366,7 +4417,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4381,7 +4431,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -4396,7 +4445,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -4431,7 +4479,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -4440,7 +4487,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4449,7 +4495,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -4489,7 +4534,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -4519,7 +4563,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -4794,7 +4837,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -4858,7 +4900,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -4873,7 +4914,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -4949,7 +4989,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -4958,7 +4997,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -4969,7 +5007,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -4984,7 +5021,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4995,7 +5031,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5015,7 +5050,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5068,7 +5102,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -5164,7 +5197,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5176,7 +5208,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -5203,7 +5234,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5416,7 +5446,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -5618,8 +5647,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/thenify": { "version": "3.3.1", @@ -5679,7 +5707,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -5724,7 +5751,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, "engines": { "node": ">=16" }, @@ -5750,7 +5776,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5771,7 +5796,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -5783,7 +5807,6 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5873,7 +5896,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -6144,7 +6166,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6175,7 +6196,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6304,8 +6324,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.17.0", @@ -6374,7 +6393,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, diff --git a/pkgs/webview-ui/app/package.json b/pkgs/webview-ui/app/package.json index 2facfba64..356215a3e 100644 --- a/pkgs/webview-ui/app/package.json +++ b/pkgs/webview-ui/app/package.json @@ -38,8 +38,10 @@ "vitest": "^1.6.0" }, "dependencies": { + "@floating-ui/dom": "^1.6.8", "@modular-forms/solid": "^0.21.0", "@solid-primitives/storage": "^3.7.1", + "@tanstack/eslint-plugin-query": "^5.51.12", "@tanstack/solid-query": "^5.51.2", "material-icons": "^1.13.12", "nanoid": "^5.0.7", diff --git a/pkgs/webview-ui/app/src/App.tsx b/pkgs/webview-ui/app/src/App.tsx index 9c056ae8c..016cff09f 100644 --- a/pkgs/webview-ui/app/src/App.tsx +++ b/pkgs/webview-ui/app/src/App.tsx @@ -1,4 +1,4 @@ -import { createSignal, type Component } from "solid-js"; +import { createEffect, createSignal, type Component } from "solid-js"; import { Layout } from "./layout/layout"; import { Route, Router } from "./Routes"; import { Toaster } from "solid-toast"; @@ -7,9 +7,20 @@ import { makePersisted } from "@solid-primitives/storage"; // Some global state const [route, setRoute] = createSignal("machines"); +createEffect(() => { + console.log(route()); +}); + export { route, setRoute }; -const [activeURI, setActiveURI] = createSignal(null); +const [activeURI, setActiveURI] = makePersisted( + createSignal(null), + { + name: "activeURI", + storage: localStorage, + } +); + export { activeURI, setActiveURI }; const [clanList, setClanList] = makePersisted(createSignal([]), { @@ -17,8 +28,6 @@ const [clanList, setClanList] = makePersisted(createSignal([]), { storage: localStorage, }); -clanList() && setActiveURI(clanList()[0]); - export { clanList, setClanList }; const App: Component = () => { diff --git a/pkgs/webview-ui/app/src/Routes.tsx b/pkgs/webview-ui/app/src/Routes.tsx index aceef3271..a9a618fff 100644 --- a/pkgs/webview-ui/app/src/Routes.tsx +++ b/pkgs/webview-ui/app/src/Routes.tsx @@ -9,6 +9,7 @@ import { Settings } from "./routes/settings"; import { Welcome } from "./routes/welcome"; import { Deploy } from "./routes/deploy"; import { CreateMachine } from "./routes/machines/create"; +import { DiskView } from "./routes/disk/view"; export type Route = keyof typeof routes; @@ -63,6 +64,11 @@ export const routes = { label: "deploy", icon: "content_copy", }, + diskConfig: { + child: DiskView, + label: "diskConfig", + icon: "disk", + }, }; interface RouterProps { diff --git a/pkgs/webview-ui/app/src/floating/index.tsx b/pkgs/webview-ui/app/src/floating/index.tsx new file mode 100644 index 000000000..96267c82f --- /dev/null +++ b/pkgs/webview-ui/app/src/floating/index.tsx @@ -0,0 +1,128 @@ +import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"; +import type { + ComputePositionConfig, + ComputePositionReturn, + ReferenceElement, +} from "@floating-ui/dom"; +import { computePosition } from "@floating-ui/dom"; + +export interface UseFloatingOptions< + R extends ReferenceElement, + F extends HTMLElement, +> extends Partial { + whileElementsMounted?: ( + reference: R, + floating: F, + update: () => void + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + ) => void | (() => void); +} + +interface UseFloatingState extends Omit { + x?: number | null; + y?: number | null; +} + +export interface UseFloatingResult extends UseFloatingState { + update(): void; +} + +export function useFloating( + reference: () => R | undefined | null, + floating: () => F | undefined | null, + options?: UseFloatingOptions +): UseFloatingResult { + const placement = () => options?.placement ?? "bottom"; + const strategy = () => options?.strategy ?? "absolute"; + + const [data, setData] = createSignal({ + x: null, + y: null, + placement: placement(), + strategy: strategy(), + middlewareData: {}, + }); + + const [error, setError] = createSignal<{ value: unknown } | undefined>(); + + createEffect(() => { + const currentError = error(); + if (currentError) { + throw currentError.value; + } + }); + + const version = createMemo(() => { + reference(); + floating(); + return {}; + }); + + function update() { + const currentReference = reference(); + const currentFloating = floating(); + + if (currentReference && currentFloating) { + const capturedVersion = version(); + computePosition(currentReference, currentFloating, { + middleware: options?.middleware, + placement: placement(), + strategy: strategy(), + }).then( + (currentData) => { + // Check if it's still valid + if (capturedVersion === version()) { + setData(currentData); + } + }, + (err) => { + setError(err); + } + ); + } + } + + createEffect(() => { + const currentReference = reference(); + const currentFloating = floating(); + + options?.middleware; + placement(); + strategy(); + + if (currentReference && currentFloating) { + if (options?.whileElementsMounted) { + const cleanup = options.whileElementsMounted( + currentReference, + currentFloating, + update + ); + + if (cleanup) { + onCleanup(cleanup); + } + } else { + update(); + } + } + }); + + return { + get x() { + return data().x; + }, + get y() { + return data().y; + }, + get placement() { + return data().placement; + }, + get strategy() { + return data().strategy; + }, + get middlewareData() { + return data().middlewareData; + }, + update, + }; +} diff --git a/pkgs/webview-ui/app/src/layout/header.tsx b/pkgs/webview-ui/app/src/layout/header.tsx index bc9234d75..1bae01663 100644 --- a/pkgs/webview-ui/app/src/layout/header.tsx +++ b/pkgs/webview-ui/app/src/layout/header.tsx @@ -1,15 +1,20 @@ import { createQuery } from "@tanstack/solid-query"; import { activeURI, setRoute } from "../App"; import { callApi } from "../api"; -import { Show } from "solid-js"; +import { Accessor, createEffect, Show } from "solid-js"; -export const Header = () => { - const { isLoading, data } = createQuery(() => ({ - queryKey: [`${activeURI()}:meta`], +interface HeaderProps { + clan_dir: Accessor; +} +export const Header = (props: HeaderProps) => { + const { clan_dir } = props; + + const query = createQuery(() => ({ + queryKey: [clan_dir(), "meta"], queryFn: async () => { - const currUri = activeURI(); - if (currUri) { - const result = await callApi("show_clan_meta", { uri: currUri }); + const curr = clan_dir(); + if (curr) { + const result = await callApi("show_clan_meta", { uri: curr }); if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; } @@ -29,16 +34,25 @@ export const Header = () => {
-
-
-
- C - - {(name) => {name()}} - + + {(meta) => ( +
+
+
+ {meta().name.slice(0, 1)} +
+
-
-
+ )} + + + + {(meta) => [ + {meta().name}, + {meta()?.description}, + ]} + +
diff --git a/pkgs/webview-ui/app/src/layout/layout.tsx b/pkgs/webview-ui/app/src/layout/layout.tsx index 84412fc9f..616faa57d 100644 --- a/pkgs/webview-ui/app/src/layout/layout.tsx +++ b/pkgs/webview-ui/app/src/layout/layout.tsx @@ -1,7 +1,7 @@ import { Component, JSXElement, Show } from "solid-js"; import { Header } from "./header"; import { Sidebar } from "../Sidebar"; -import { clanList, route, setRoute } from "../App"; +import { activeURI, clanList, route, setRoute } from "../App"; interface LayoutProps { children: JSXElement; @@ -18,7 +18,7 @@ export const Layout: Component = (props) => { />
-
+
{props.children}
diff --git a/pkgs/webview-ui/app/src/routes/clan/editClan.tsx b/pkgs/webview-ui/app/src/routes/clan/editClan.tsx new file mode 100644 index 000000000..6494d1e7d --- /dev/null +++ b/pkgs/webview-ui/app/src/routes/clan/editClan.tsx @@ -0,0 +1,175 @@ +import { OperationResponse, callApi, pyApi } from "@/src/api"; +import { Accessor, Show, Switch, Match } from "solid-js"; +import { + SubmitHandler, + createForm, + required, + reset, +} from "@modular-forms/solid"; +import toast from "solid-toast"; +import { createQuery } from "@tanstack/solid-query"; + +type CreateForm = Meta; + +interface EditClanFormProps { + directory: Accessor; + done: () => void; +} +export const EditClanForm = (props: EditClanFormProps) => { + const { directory } = props; + const details = createQuery(() => ({ + queryKey: [directory(), "meta"], + queryFn: async () => { + const result = await callApi("show_clan_meta", { uri: directory() }); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + }, + })); + + return ( + + + {(data) => ( + + )} + + + ); +}; + +interface FinalEditClanFormProps { + initial: CreateForm; + directory: string; + done: () => void; +} +export const FinalEditClanForm = (props: FinalEditClanFormProps) => { + const [formStore, { Form, Field }] = createForm({ + initialValues: props.initial, + }); + + const handleSubmit: SubmitHandler = async (values, event) => { + await toast.promise( + (async () => { + await callApi("update_clan_meta", { + options: { + directory: props.directory, + meta: values, + }, + }); + })(), + { + loading: "Updating clan...", + success: "Clan Successfully updated", + error: "Failed to update clan", + } + ); + props.done(); + }; + + return ( +
+
+ + {(field, props) => ( + <> +
+ + group + + } + > + {(icon) => ( + Clan Logo + )} + +
+ + )} +
+
+ + {(field, props) => ( + + )} + + + {(field, props) => ( + + )} + + { +
+ +
+ } +
+
+
+ ); +}; + +type Meta = Extract< + OperationResponse<"show_clan_meta">, + { status: "success" } +>["data"]; diff --git a/pkgs/webview-ui/app/src/routes/disk/view.tsx b/pkgs/webview-ui/app/src/routes/disk/view.tsx new file mode 100644 index 000000000..43e2f4aff --- /dev/null +++ b/pkgs/webview-ui/app/src/routes/disk/view.tsx @@ -0,0 +1,33 @@ +import { callApi } from "@/src/api"; +import { activeURI } from "@/src/App"; +import { createQuery } from "@tanstack/solid-query"; +import { createEffect } from "solid-js"; +import toast from "solid-toast"; + +export function DiskView() { + const query = createQuery(() => ({ + queryKey: ["disk", activeURI()], + queryFn: async () => { + const currUri = activeURI(); + if (currUri) { + // Example of calling an API + const result = await callApi("get_inventory", { base_path: currUri }); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + } + }, + })); + createEffect(() => { + // Example debugging the data + console.log(query); + }); + return ( +
+

Configure Disk

+

+ Select machine then configure the disk. Required before installing for + the first time. +

+
+ ); +} diff --git a/pkgs/webview-ui/app/src/routes/settings/index.tsx b/pkgs/webview-ui/app/src/routes/settings/index.tsx index 29eb2846b..8fa7869f7 100644 --- a/pkgs/webview-ui/app/src/routes/settings/index.tsx +++ b/pkgs/webview-ui/app/src/routes/settings/index.tsx @@ -12,8 +12,19 @@ import { setRoute, clanList, } from "@/src/App"; -import { For, Show } from "solid-js"; +import { + createEffect, + createSignal, + For, + Match, + Setter, + Show, + Switch, +} from "solid-js"; import { createQuery } from "@tanstack/solid-query"; +import { useFloating } from "@/src/floating"; +import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom"; +import { EditClanForm } from "../clan/editClan"; export const registerClan = async () => { try { @@ -41,9 +52,10 @@ export const registerClan = async () => { interface ClanDetailsProps { clan_dir: string; + setEditURI: Setter; } const ClanDetails = (props: ClanDetailsProps) => { - const { clan_dir } = props; + const { clan_dir, setEditURI } = props; const details = createQuery(() => ({ queryKey: [clan_dir, "meta"], @@ -54,10 +66,41 @@ const ClanDetails = (props: ClanDetailsProps) => { }, })); + const [reference, setReference] = createSignal(); + const [floating, setFloating] = createSignal(); + + // `position` is a reactive object. + const position = useFloating(reference, floating, { + placement: "top", + + // pass options. Ensure the cleanup function is returned. + whileElementsMounted: (reference, floating, update) => + autoUpdate(reference, floating, update, { + animationFrame: true, + }), + middleware: [ + offset(5), + shift(), + flip(), + + hide({ + strategy: "referenceHidden", + }), + ], + }); + return (
+ +
+ +
-
Clan URI
+
{clan_dir}
-
- {details.data?.name} -
+
{details.data?.name}
- {clan_dir}
} - > -
- {details.data?.description} -
+ +
{details.data?.description}
); }; export const Settings = () => { + const [editURI, setEditURI] = createSignal(null); + return (
-
-
-
Registered Clans
- -
-
- - {(value) => } - -
-
+ + + {(uri) => ( + { + setEditURI(null); + }} + /> + )} + + +
+
+
Registered Clans
+ +
+
+ + {(value) => ( + + )} + +
+
+
+
); }; diff --git a/pkgs/webview-ui/flake-module.nix b/pkgs/webview-ui/flake-module.nix index be51c7f51..0fddc56e1 100644 --- a/pkgs/webview-ui/flake-module.nix +++ b/pkgs/webview-ui/flake-module.nix @@ -16,7 +16,7 @@ npmDeps = pkgs.fetchNpmDeps { src = ./app; - hash = "sha256-/PFSBAIodZjInElYoNsDQUV4isxmcvL3YM1hzAmdDWA="; + hash = "sha256-n9IXcfCpydykoYD+P/YNtNIwrvgJTZND0kg7oXBfmJ0="; }; # The prepack script runs the build script, which we'd rather do in the build phase. npmPackFlags = [ "--ignore-scripts" ];