diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..97f5da8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Agent Guidelines for Phoenix.React + +## Commands +- **Build**: `mix deps.get && npm install` +- **Format**: `mix format` (uses .formatter.exs config) +- **Test**: `mix test` (requires 3s delay for server startup in test_helper.exs) +- **Single test**: `mix test path/to/test.exs:line_number` +- **Bundle React components**: `mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js` + +## Code Style +- **Elixir**: Follow standard Elixir conventions, use `mix format` +- **Module names**: PascalCase (e.g., `Phoenix.React.Server`) +- **Function names**: snake_case +- **Types**: Use `@type` and `@spec` for all public functions +- **Error handling**: Return `{:ok, result}` or `{:error, reason}` tuples +- **Imports**: Alias modules at top, use qualified calls when needed +- **GenServer**: Use `@impl true` for callback implementations +- **Documentation**: Include `@moduledoc` and `@doc` for public modules/functions +- **React components**: Export `Component` function, use JSX syntax in .js files \ No newline at end of file diff --git a/README.md b/README.md index 79c380c..9f6c7ba 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,44 @@ config :phoenix_react_server, Phoenix.React, Supported `runtime` - [x] `Phoenix.React.Runtime.Bun` -- [ ] `Phoenix.React.Runtime.Deno` +- [x] `Phoenix.React.Runtime.Deno` + +### Using Deno Runtime + +To use Deno instead of Bun, configure the runtime and its specific settings: + +```elixir +config :phoenix_react_server, Phoenix.React, + runtime: Phoenix.React.Runtime.Deno, + component_base: Path.expand("../assets/component", __DIR__), + cache_ttl: 60 + +# Deno-specific configuration +config :phoenix_react_server, Phoenix.React.Runtime.Deno, + cmd: System.find_executable("deno"), + server_js: Path.expand("../priv/react/server.js", __DIR__), + port: 5125, + env: :dev # Use :prod for production +``` + +**Deno Requirements:** +- Deno 2.x (recommended) +- Components must use `.jsx` file extension for proper JSX parsing +- Deno automatically downloads npm packages via `--node-modules-dir` flag + +**Environment Variable Switching:** +You can also use environment variable to switch runtimes: + +```elixir +runtime = + case System.get_env("REACT_RUNTIME", "bun") do + "bun" -> Phoenix.React.Runtime.Bun + "deno" -> Phoenix.React.Runtime.Deno + _ -> Phoenix.React.Runtime.Bun + end + +config :phoenix_react_server, Phoenix.React, runtime: runtime +``` Add Render Server in your application Supervisor tree. diff --git a/config/test.exs b/config/test.exs index d4fa561..3b31813 100644 --- a/config/test.exs +++ b/config/test.exs @@ -3,7 +3,7 @@ import Config config :phoenix_react_server, Phoenix.React, runtime: Phoenix.React.Runtime.Bun, component_base: Path.expand("../test/fixtures", __DIR__), - render_timeout: 5_000, + render_timeout: 10_000, cache_ttl: 60 config :phoenix_react_server, Phoenix.React.Runtime.Bun, port: 12457 diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..d57b9ae --- /dev/null +++ b/deno.lock @@ -0,0 +1,36 @@ +{ + "version": "4", + "specifiers": { + "npm:react-dom@*": "19.2.0_react@19.2.0", + "npm:react-dom@19": "19.2.0_react@19.2.0", + "npm:react@*": "19.2.0", + "npm:react@19": "19.2.0" + }, + "npm": { + "react-dom@19.2.0_react@19.2.0": { + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "dependencies": [ + "react", + "scheduler" + ] + }, + "react@19.2.0": { + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==" + }, + "scheduler@0.27.0": { + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + } + }, + "remote": { + "https://deno.land/std@0.208.0/async/delay.ts": "a6142eb44cdd856b645086af2b811b1fcce08ec06bb7d50969e6a872ee9b8659", + "https://deno.land/std@0.208.0/http/server.ts": "f3cde6672e631d3e00785743cfa96bfed275618c0352c5ae84abbe5a2e0e4afc" + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:react-dom@19", + "npm:react@19" + ] + } + } +} diff --git a/devenv.nix b/devenv.nix index c78e5bb..a3a88aa 100644 --- a/devenv.nix +++ b/devenv.nix @@ -24,6 +24,9 @@ in languages.javascript.bun.enable = true; languages.javascript.bun.package = pkgs-stable.bun; + languages.deno.enable = true; + languages.deno.package = pkgs-stable.deno; + scripts.hello.exec = '' figlet -w 120 $GREET | lolcat ''; diff --git a/lib/phoenix/mix/build/bun.ex b/lib/phoenix/mix/build/bun.ex index f1cb167..97415af 100644 --- a/lib/phoenix/mix/build/bun.ex +++ b/lib/phoenix/mix/build/bun.ex @@ -43,7 +43,7 @@ defmodule Mix.Tasks.Phx.React.Bun.Bundle do {basename, abs_path} end) - quoted = EEx.compile_file("#{__DIR__}/server.js.eex") + quoted = EEx.compile_file("#{__DIR__}/server_bun.eex") {result, _bindings} = Code.eval_quoted(quoted, files: files, base_dir: base_dir) tmp_file = "#{cd}/server.js" File.write!(tmp_file, result) diff --git a/lib/phoenix/mix/build/deno.ex b/lib/phoenix/mix/build/deno.ex new file mode 100644 index 0000000..5f8a39b --- /dev/null +++ b/lib/phoenix/mix/build/deno.ex @@ -0,0 +1,138 @@ +defmodule Mix.Tasks.Phx.React.Deno.Bundle do + @moduledoc """ + Create server.js bundle for `deno` runtime, + bundle all components and render server in one file for otp release. + + ## Usage + + ```shell + mix phx.react.deno.bundle --component-base=assets/component --output=priv/react/server.js + ``` + + """ + require Logger + + use Mix.Task + + @shortdoc "Bundle components into server.js" + def run(args) do + {opts, _argv} = + OptionParser.parse!(args, strict: [component_base: :string, output: :string, cd: :string]) + + component_base = Keyword.get(opts, :component_base) + base_dir = Path.absname(component_base, File.cwd!()) |> Path.expand() + + components = + if File.dir?(base_dir) do + find_files(base_dir) + else + raise ArgumentError, "component_base dir does not exist: #{base_dir}" + end + + output = Keyword.get(opts, :output) + Logger.info("Bundle component in directory [#{component_base}] into #{output}") + + cd = Keyword.get(opts, :cd, File.cwd!()) + + # Create JSX files for Deno + jsx_dir = Path.join(Path.dirname(output), "jsx_components") + File.mkdir_p!(jsx_dir) + # Clean up first + File.rm_rf!(jsx_dir) + File.mkdir_p!(jsx_dir) + + files = + components + |> Enum.map(fn abs_path -> + filename = Path.relative_to(abs_path, base_dir) + ext = Path.extname(filename) + basename = Path.basename(filename, ext) + + # Create JSX version for Deno + jsx_path = Path.join(jsx_dir, "#{basename}.jsx") + File.cp!(abs_path, jsx_path) + + {basename, jsx_path} + end) + + quoted = EEx.compile_file("#{__DIR__}/server_deno.js.eex") + + {result, _bindings} = + Code.eval_quoted(quoted, files: files, base_dir: base_dir, output: output) + + _outdir = Path.dirname(output) + + if File.exists?(output) do + File.rm!(output) + end + + # Check if this is a source file (ends with _source.js) or binary output + if String.ends_with?(output, "_source.js") do + # For development, fix import paths to be relative and write the source file + jsx_dir = Path.join(Path.dirname(output), "jsx_components") + result = String.replace(result, "\"#{jsx_dir}/", "\"./jsx_components/") + File.write!(output, result) + Logger.info("Created Deno source file: #{output}") + else + # For production, create binary + tmp_file = "#{cd}/server_deno.js" + File.write!(tmp_file, result) + + # Deno 2.x removed bundle, use compile instead + {out, code} = + System.cmd("deno", ["compile", "--output", output, tmp_file], cd: cd) + + Logger.info(~s[cd #{cd}; deno compile --output #{output} #{tmp_file}]) + Logger.info("out #{code}: #{out}") + + if code != 0 do + throw("deno compile failed(#{code})") + end + + File.rm!(tmp_file) + end + + # Clean up JSX files only for production builds + unless String.ends_with?(output, "_source.js") do + File.rm_rf!(jsx_dir) + end + rescue + error -> + Logger.error("Build failed: #{Exception.format(:error, error, __STACKTRACE__)}") + reraise error, __STACKTRACE__ + catch + :throw, error -> + Logger.error("Build failed: #{inspect(error)}") + throw(error) + + :exit, error -> + Logger.error("Build failed: #{inspect(error)}") + exit(error) + end + + def find_files(dir) do + find_files(dir, []) + end + + defp find_files(dir, acc) do + case File.ls(dir) do + {:ok, entries} -> + entries + |> Enum.reduce(acc, fn entry, acc -> + path = Path.join(dir, entry) + + cond do + # Recurse into subdirectories + File.dir?(path) -> find_files(path, acc) + # Collect files + File.regular?(path) -> [path | acc] + true -> acc + end + end) + + # Ignore errors (e.g., permission issues) + {:error, _} -> + acc + end + end +end diff --git a/lib/phoenix/mix/build/server.js.eex b/lib/phoenix/mix/build/server_bun.eex similarity index 93% rename from lib/phoenix/mix/build/server.js.eex rename to lib/phoenix/mix/build/server_bun.eex index 300c5d0..0d9487f 100644 --- a/lib/phoenix/mix/build/server.js.eex +++ b/lib/phoenix/mix/build/server_bun.eex @@ -1,5 +1,6 @@ import { serve, readableStreamToJSON, readableStreamToText, escapeHTML } from 'bun'; import { renderToReadableStream, renderToString, renderToStaticMarkup } from 'react-dom/server'; +import React from 'react'; const __comMap = {}; <%= for {{name, file}, idx} <- Enum.with_index(files) do %> @@ -7,11 +8,12 @@ import { Component as __component_<%= idx %> } from "<%= file %>"; __comMap["<%= name %>"] = __component_<%= idx %>; <% end %> -const { COMPONENT_BASE, BUN_ENV } = process.env; +const { COMPONENT_BASE, BUN_ENV, BUN_PORT } = process.env; const isDev = BUN_ENV === 'development'; const server = serve({ + port: parseInt(BUN_PORT || "5225"), development: isDev, async fetch(req) { try { @@ -47,7 +49,7 @@ const server = serve({ }, }); } - const jsxNode = ; + const jsxNode = React.createElement(Component, props); const html = renderToStaticMarkup(jsxNode); return new Response(html, { headers: { @@ -60,7 +62,7 @@ const server = serve({ const props = await readableStreamToJSON(bodyStream); const fileName = pathname.replace(/^\/render_to_string\//, ''); const Component = __comMap[fileName]; - const jsxNode = ; + const jsxNode = React.createElement(Component, props); const html = renderToString(jsxNode); return new Response(html, { headers: { @@ -73,7 +75,7 @@ const server = serve({ const props = await readableStreamToJSON(bodyStream); const fileName = pathname.replace(/^\/render_to_readable_stream\//, ''); const Component = __comMap[fileName]; - const jsxNode = ; + const jsxNode = React.createElement(Component, props); const stream = await renderToReadableStream(jsxNode); return new Response(stream, { headers: { diff --git a/lib/phoenix/mix/build/server_deno.js.eex b/lib/phoenix/mix/build/server_deno.js.eex new file mode 100644 index 0000000..cf39cf2 --- /dev/null +++ b/lib/phoenix/mix/build/server_deno.js.eex @@ -0,0 +1,184 @@ +import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; +import React from "npm:react"; +import { renderToReadableStream, renderToString, renderToStaticMarkup } from "npm:react-dom/server"; + +const __comMap = {}; +<%= for {{name, file}, idx} <- Enum.with_index(files) do %> +import { Component as __component_<%= idx %> } from "<%= file %>"; +__comMap["<%= name %>"] = __component_<%= idx %>; +<% end %> + +const { COMPONENT_BASE, DENO_ENV } = Deno.env.toObject(); + +const isDev = DENO_ENV === 'development'; + +const port = parseInt(Deno.env.get("PORT") || "5226"); + +const handler = async (req) => { + try { + let bodyStream = req.body; + if (isDev) { + const bodyText = await req.text(); + console.log('Request: ', req.method, req.url, bodyText); + bodyStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(bodyText)); + controller.close(); + } + }); + } + const { url } = req; + const uri = new URL(url); + const { pathname } = uri; + + // Security: Validate pathname to prevent path traversal + if (pathname.includes('..') || pathname.includes('\\')) { + return new Response('Invalid path', { status: 400 }); + } + + if (pathname.startsWith('/stop')) { + return new Response('{"message":"ok"}', { + headers: { + "Content-Type": "application/json", + }, + }); + } + + if (pathname.startsWith('/render_to_static_markup/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_static_markup\//, ''); + + // Security: Validate component name + if (!/^[a-zA-Z0-9_-]+$/.test(fileName)) { + return new Response('Invalid component name', { status: 400 }); + } + + const Component = __comMap[fileName]; + if (!Component) { + return new Response(`Not Found, component not found.`, { + status: 404, + headers: { + "Content-Type": "text/html", + }, + }); + } + const jsxNode = React.createElement(Component, props); + const html = renderToStaticMarkup(jsxNode); + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + if (pathname.startsWith('/render_to_string/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_string\//, ''); + + // Security: Validate component name + if (!/^[a-zA-Z0-9_-]+$/.test(fileName)) { + return new Response('Invalid component name', { status: 400 }); + } + + const Component = __comMap[fileName]; + const jsxNode = React.createElement(Component, props); + const html = renderToString(jsxNode); + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + if (pathname.startsWith('/render_to_readable_stream/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_readable_stream\//, ''); + + // Security: Validate component name + if (!/^[a-zA-Z0-9_-]+$/.test(fileName)) { + return new Response('Invalid component name', { status: 400 }); + } + + const Component = __comMap[fileName]; + const jsxNode = React.createElement(Component, props); + const stream = await renderToReadableStream(jsxNode); + return new Response(stream, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + return new Response(`Not Found, not matched request.`, { + status: 404, + headers: { + "Content-Type": "text/html", + }, + }); + } catch(error) { + const html = ` + + `; + return new Response(html, { + status: 500, + headers: { + "Content-Type": "text/html", + }, + }); + } +}; + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +console.log(`Server started at http://localhost:${port}`); +console.log(`COMPONENT_BASE`, COMPONENT_BASE); +console.log(`DENO_ENV`, DENO_ENV); + +const ppid = Deno.pid; +const parentCheckInterval = parseInt(Deno.env.get("PARENT_CHECK_INTERVAL") || "5000"); + +const checkParentInterval = setInterval(() => { + try { + // Enhanced validation: ensure PPID is valid and different from current process + if (ppid && ppid > 1 && ppid !== Deno.pid) { + Deno.kill(ppid, "0"); + } else { + console.log("Invalid PPID detected. Shutting down server..."); + clearInterval(checkParentInterval); + Deno.exit(0); + } + } catch (e) { + console.log("Parent process exited. Shutting down server..."); + clearInterval(checkParentInterval); + Deno.exit(0); + } +}, parentCheckInterval); + +const shutdown = async (signal) => { + console.log(`\nReceived ${signal}. Cleaning up...`); + clearInterval(checkParentInterval); + console.log("Cleanup done. Exiting."); + Deno.exit(0); +}; + +Deno.addSignalListener("SIGINT", () => { + shutdown("SIGINT"); +}); + +Deno.addSignalListener("SIGTERM", () => { + shutdown("SIGTERM"); +}); + +await serve(handler, { port }); \ No newline at end of file diff --git a/lib/phoenix/react.ex b/lib/phoenix/react.ex index b273a05..8588503 100644 --- a/lib/phoenix/react.ex +++ b/lib/phoenix/react.ex @@ -37,10 +37,10 @@ defmodule Phoenix.React do cache_ttl: 60 ``` - Supported `runtime` + Supported `runtime` - - [x] `Phoenix.React.Runtime.Bun` - - [ ] `Phoenix.React.Runtime.Deno` + - [x] `Phoenix.React.Runtime.Bun` + - [x] `Phoenix.React.Runtime.Deno` Add Render Server in your application Supervisor tree. @@ -215,24 +215,43 @@ defmodule Phoenix.React do } ``` - ## Run in release mode + ## Run in release mode - Bundle components with server.js to one file. + Bundle components with server.js to one file. - ```shell - mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js - ``` + ### For Bun runtime - Config `runtime` to `Phoenix.React.Runtime.Bun` in `runtime.exs` + ```shell + mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js + ``` - ```elixir + Config `runtime` to `Phoenix.React.Runtime.Bun` in `runtime.exs` - config :phoenix_react_server, Phoenix.React.Runtime.Bun, - cmd: System.find_executable("bun"), - server_js: Path.expand("../priv/react/server.js", __DIR__), - port: 12666, - env: :prod - ``` + ```elixir + + config :phoenix_react_server, Phoenix.React.Runtime.Bun, + cmd: System.find_executable("bun"), + server_js: Path.expand("../priv/react/server.js", __DIR__), + port: 12666, + env: :prod + ``` + + ### For Deno runtime + + ```shell + mix phx.react.deno.bundle --component-base=assets/component --output=priv/react/server.js + ``` + + Config `runtime` to `Phoenix.React.Runtime.Deno` in `runtime.exs` + + ```elixir + + config :phoenix_react_server, Phoenix.React.Runtime.Deno, + cmd: System.find_executable("deno"), + server_js: Path.expand("../priv/react/server.js", __DIR__), + port: 12667, + env: :prod + ``` ## Hydrate at client side with CDN @@ -267,6 +286,22 @@ defmodule Phoenix.React do @impl true def init(_init_arg) do + # Start HTTP client for runtime communication + {:ok, _} = Application.ensure_all_started(:inets) + + case :httpc.start_service([{:profile, :default}]) do + {:ok, _pid} -> + :ok + + {:error, {:already_started, _pid}} -> + :ok + + error -> + require Logger + Logger.error("Failed to start HTTP client: #{inspect(error)}") + raise "Failed to start HTTP client: #{inspect(error)}" + end + children = [ {Phoenix.React.Cache, []}, {Phoenix.React.Runtime, []}, diff --git a/lib/phoenix/react/config.ex b/lib/phoenix/react/config.ex new file mode 100644 index 0000000..e63b29a --- /dev/null +++ b/lib/phoenix/react/config.ex @@ -0,0 +1,151 @@ +defmodule Phoenix.React.Config do + @moduledoc """ + Centralized configuration management for Phoenix.React runtimes. + + This module provides default values and validation for all configurable + parameters across different runtime implementations. + """ + + @doc """ + Default configuration values for all runtimes. + """ + def defaults do + %{ + # Common defaults + env: :dev, + render_timeout: 5000, + cache_ttl: 60, + + # Bun-specific defaults + bun: %{ + port: 5225, + server_js: "bun/server.js", + cmd: "bun" + }, + + # Deno-specific defaults + deno: %{ + port: 5226, + server_js: "deno/server.js", + cmd: "deno", + write_dirs: ["/tmp", "/var/tmp"], + parent_check_interval: 5000, + node_modules_dir: true + }, + + # File watching defaults + file_watcher: %{ + throttle_ms: 3000, + debounce_ms: 100 + }, + + # Security defaults + security: %{ + max_component_name_length: 100, + allowed_component_name_pattern: ~r/^[a-zA-Z0-9_-]+$/, + # 1MB + max_request_size: 1_048_576, + request_timeout_ms: 30000 + } + } + end + + @doc """ + Gets configuration for a specific runtime with defaults applied. + """ + def runtime_config(runtime_name, user_config \\ %{}) do + runtime_defaults = Map.get(defaults(), runtime_name, %{}) + common_defaults = Map.delete(defaults(), runtime_name) + + merged = + common_defaults + |> Map.merge(runtime_defaults) + |> Map.merge(user_config) + + validate_runtime_config(runtime_name, merged) + end + + @doc """ + Validates runtime-specific configuration. + """ + def validate_runtime_config(runtime_name, config) when is_map(config) do + errors = + [] + |> then(&validate_port(&1, config[:port])) + |> then(&validate_env(&1, config[:env])) + |> then(&validate_timeout(&1, config[:render_timeout])) + |> then(&validate_write_dirs(&1, config[:write_dirs], runtime_name)) + |> then(&validate_parent_check_interval(&1, config[:parent_check_interval], runtime_name)) + + if Enum.empty?(errors) do + {:ok, config} + else + {:error, + "#{String.upcase(Atom.to_string(runtime_name))} configuration errors: #{Enum.join(errors, "; ")}"} + end + end + + @doc """ + Gets security configuration. + """ + def security_config(overrides \\ %{}) do + defaults().security + |> Map.merge(overrides) + end + + @doc """ + Gets file watcher configuration. + """ + def file_watcher_config(overrides \\ %{}) do + defaults().file_watcher + |> Map.merge(overrides) + end + + # Private validation functions + + defp validate_port(errors, nil), do: ["port is required" | errors] + + defp validate_port(errors, port) when is_integer(port) and port > 0 and port <= 65535, + do: errors + + defp validate_port(errors, _), do: ["port must be between 1 and 65535" | errors] + + defp validate_env(errors, nil), do: ["env is required" | errors] + defp validate_env(errors, env) when env in [:dev, :prod], do: errors + defp validate_env(errors, _), do: ["env must be :dev or :prod" | errors] + + defp validate_timeout(errors, nil), do: errors + defp validate_timeout(errors, timeout) when is_integer(timeout) and timeout > 0, do: errors + defp validate_timeout(errors, _), do: ["render_timeout must be a positive integer" | errors] + + defp validate_write_dirs(errors, nil, :deno), + do: ["write_dirs is required for Deno runtime" | errors] + + defp validate_write_dirs(errors, dirs, :deno) when is_list(dirs) and length(dirs) > 0, + do: errors + + defp validate_write_dirs(errors, _, :deno), + do: ["write_dirs must be a non-empty list for Deno runtime" | errors] + + defp validate_write_dirs(errors, _, _), do: errors + + defp validate_parent_check_interval(errors, nil, :deno), + do: ["parent_check_interval is required for Deno runtime" | errors] + + defp validate_parent_check_interval(errors, interval, :deno) + when is_integer(interval) and interval >= 1000, + do: errors + + defp validate_parent_check_interval(errors, _, :deno), + do: ["parent_check_interval must be at least 1000ms for Deno runtime" | errors] + + defp validate_parent_check_interval(errors, _, _), do: errors + + @doc """ + Converts configuration to keyword list for backward compatibility. + """ + def to_keyword_list(config) when is_map(config) do + config + |> Enum.map(fn {k, v} -> {k, v} end) + end +end diff --git a/lib/phoenix/react/monitoring.ex b/lib/phoenix/react/monitoring.ex new file mode 100644 index 0000000..7388576 --- /dev/null +++ b/lib/phoenix/react/monitoring.ex @@ -0,0 +1,219 @@ +defmodule Phoenix.React.Monitoring do + @moduledoc """ + Monitoring and metrics collection for Phoenix.React runtimes. + + This module provides: + - Performance metrics collection + - Health checks + - Runtime status monitoring + - Error tracking + """ + + require Logger + + @doc """ + Records a render request with timing information. + """ + @spec record_render(String.t(), atom(), non_neg_integer(), :ok | :error) :: :ok + def record_render(component, method, duration_ms, result) do + metadata = %{ + component: component, + method: method, + duration_ms: duration_ms, + result: result, + timestamp: DateTime.utc_now() + } + + Logger.debug("React render: #{inspect(metadata)}") + + # Could integrate with telemetry here + :telemetry.execute([:phoenix, :react, :render], %{duration: duration_ms}, metadata) + end + + @doc """ + Records a runtime startup event. + """ + @spec record_runtime_startup(String.t(), non_neg_integer()) :: :ok + def record_runtime_startup(runtime_name, port) do + metadata = %{ + runtime: runtime_name, + port: port, + timestamp: DateTime.utc_now() + } + + Logger.info("React runtime started: #{inspect(metadata)}") + :telemetry.execute([:phoenix, :react, :runtime_startup], %{}, metadata) + end + + @doc """ + Records a runtime shutdown event. + """ + @spec record_runtime_shutdown(String.t(), term()) :: :ok + def record_runtime_shutdown(runtime_name, reason) do + metadata = %{ + runtime: runtime_name, + reason: reason, + timestamp: DateTime.utc_now() + } + + Logger.info("React runtime shutdown: #{inspect(metadata)}") + :telemetry.execute([:phoenix, :react, :runtime_shutdown], %{}, metadata) + end + + @doc """ + Records a file change event. + """ + @spec record_file_change(String.t(), String.t()) :: :ok + def record_file_change(path, action \\ "changed") do + metadata = %{ + path: path, + action: action, + timestamp: DateTime.utc_now() + } + + Logger.debug("React file change: #{inspect(metadata)}") + :telemetry.execute([:phoenix, :react, :file_change], %{}, metadata) + end + + @doc """ + Records a build event. + """ + @spec record_build(String.t(), non_neg_integer(), :ok | :error) :: :ok + def record_build(runtime_name, duration_ms, result) do + metadata = %{ + runtime: runtime_name, + duration_ms: duration_ms, + result: result, + timestamp: DateTime.utc_now() + } + + Logger.info("React build: #{inspect(metadata)}") + :telemetry.execute([:phoenix, :react, :build], %{duration: duration_ms}, metadata) + end + + @doc """ + Performs a health check on the runtime. + """ + @spec health_check(String.t(), non_neg_integer()) :: {:ok, map()} | {:error, term()} + def health_check(runtime_name, port) do + start_time = System.monotonic_time(:millisecond) + + url = "http://localhost:#{port}/stop" + + case :httpc.request(:get, {String.to_charlist(url), []}, [], []) do + {:ok, {{_version, status_code, _status_text}, _headers, _body}} + when status_code in 200..299 -> + duration = System.monotonic_time(:millisecond) - start_time + + metadata = %{ + runtime: runtime_name, + port: port, + status: "healthy", + response_time_ms: duration, + timestamp: DateTime.utc_now() + } + + Logger.debug("React health check: #{inspect(metadata)}") + {:ok, metadata} + + {:ok, {{_version, status_code, _status_text}, _headers, body}} -> + duration = System.monotonic_time(:millisecond) - start_time + error = "HTTP #{status_code}: #{body}" + + metadata = %{ + runtime: runtime_name, + port: port, + status: "unhealthy", + error: error, + response_time_ms: duration, + timestamp: DateTime.utc_now() + } + + Logger.warning("React health check failed: #{inspect(metadata)}") + {:error, error} + + {:error, reason} -> + duration = System.monotonic_time(:millisecond) - start_time + + metadata = %{ + runtime: runtime_name, + port: port, + status: "unreachable", + error: reason, + response_time_ms: duration, + timestamp: DateTime.utc_now() + } + + Logger.error("React health check failed: #{inspect(metadata)}") + {:error, reason} + end + end + + @doc """ + Gets runtime statistics. + """ + @spec get_runtime_stats(String.t()) :: map() + def get_runtime_stats(runtime_name) do + # This could be expanded to collect more detailed stats + %{ + runtime: runtime_name, + uptime: get_runtime_uptime(runtime_name), + memory_usage: get_memory_usage(), + process_count: length(Process.list()), + timestamp: DateTime.utc_now() + } + end + + @doc """ + Measures execution time of a function and records it. + """ + @spec measure(String.t(), atom(), function()) :: any() + def measure(operation_name, telemetry_event, fun) when is_function(fun, 0) do + start_time = System.monotonic_time(:millisecond) + + try do + result = fun.() + duration = System.monotonic_time(:millisecond) - start_time + + metadata = %{ + operation: operation_name, + duration_ms: duration, + result: :ok, + timestamp: DateTime.utc_now() + } + + Logger.debug("Operation measured: #{inspect(metadata)}") + :telemetry.execute(telemetry_event, %{duration: duration}, metadata) + + result + rescue + error -> + duration = System.monotonic_time(:millisecond) - start_time + + metadata = %{ + operation: operation_name, + duration_ms: duration, + result: :error, + error: Exception.format(:error, error), + timestamp: DateTime.utc_now() + } + + Logger.error("Operation failed: #{inspect(metadata)}") + :telemetry.execute(telemetry_event, %{duration: duration}, metadata) + + reraise error, __STACKTRACE__ + end + end + + # Private helper functions + + defp get_runtime_uptime(_runtime_name) do + # This would need to be implemented based on how we track startup time + # For now, return a placeholder + "unknown" + end + + defp get_memory_usage do + :erlang.memory() + end +end diff --git a/lib/phoenix/react/runtime/bun.ex b/lib/phoenix/react/runtime/bun.ex index 707d3bb..f9fdef2 100644 --- a/lib/phoenix/react/runtime/bun.ex +++ b/lib/phoenix/react/runtime/bun.ex @@ -20,6 +20,7 @@ defmodule Phoenix.React.Runtime.Bun do require Logger use Phoenix.React.Runtime + import Phoenix.React.Runtime.Common def start_link(init_arg) do GenServer.start_link(__MODULE__, init_arg, name: __MODULE__) @@ -39,93 +40,92 @@ defmodule Phoenix.React.Runtime.Bun do @impl true @spec handle_continue(:start_port, Phoenix.React.Runtime.t()) :: {:noreply, Phoenix.React.Runtime.t()} + | {:stop, reason :: term, Phoenix.React.Runtime.t()} def handle_continue(:start_port, %Runtime{component_base: component_base} = state) do if config()[:env] == :dev do start_file_watcher(component_base) Phoenix.React.Runtime.FileWatcher.set_ref(self()) end - port = start(component_base: component_base) + case start(component_base: component_base) do + port when is_port(port) -> + Logger.debug( + "Bun.Server started on port: #{inspect(port)} and OS pid: #{get_port_os_pid(port)}" + ) - Logger.debug( - "Bun.Server started on port: #{inspect(port)} and OS pid: #{get_port_os_pid(port)}" - ) + Phoenix.React.Server.set_runtime_process(self()) - Phoenix.React.Server.set_runtime_process(self()) + {:noreply, %Runtime{state | runtime_port: port}} - {:noreply, %Runtime{state | runtime_port: port}} + {:error, reason} -> + Logger.error("Failed to start Bun server: #{inspect(reason)}") + {:stop, reason, state} + end end @impl true def config() do - cfg = Application.get_env(:phoenix_react_server, Phoenix.React.Runtime.Bun, []) - cmd = cfg[:cmd] || System.find_executable("bun") - - server_js = - cfg[:server_js] || Path.expand("bun/server.js", :code.priv_dir(:phoenix_react_server)) - - [ - {:cd, cfg[:cd] || File.cwd!()}, - {:cmd, cmd}, - {:server_js, server_js}, - {:port, cfg[:port] || 5225}, - {:env, cfg[:env] || :dev} - ] + user_config = Application.get_env(:phoenix_react_server, Phoenix.React.Runtime.Bun, []) + + # Convert user config to map for new config system + user_config_map = + user_config + |> Enum.into(%{}) + |> Map.put(:cd, Keyword.get(user_config, :cd, File.cwd!())) + |> Map.put(:cmd, Keyword.get(user_config, :cmd, System.find_executable("bun"))) + |> Map.put( + :server_js, + Keyword.get( + user_config, + :server_js, + Path.expand("bun/server.js", :code.priv_dir(:phoenix_react_server)) + ) + ) + + case Phoenix.React.Config.runtime_config(:bun, user_config_map) do + {:ok, config} -> Phoenix.React.Config.to_keyword_list(config) + {:error, reason} -> raise ArgumentError, reason + end end @impl true - def start(component_base: component_base) do - cd = config()[:cd] - cmd = config()[:cmd] - bun_port = Integer.to_string(config()[:port]) - args = ["--port", bun_port, config()[:server_js]] - - is_dev = config()[:env] == :dev + def start(component_base: _component_base) do + config = config() + cmd = config[:cmd] + bun_port = Integer.to_string(config[:port]) + server_js = config[:server_js] - bun_env = if(is_dev, do: "development", else: "production") + args = ["--port", bun_port, server_js] args = - if config()[:env] == :dev do + if config[:env] == :dev do ["--watch" | args] else args end - env = [ - {~c"PORT", ~c"#{bun_port}"}, - {~c"BUN_PORT", ~c"#{bun_port}"}, - {~c"BUN_ENV", ~c"#{bun_env}"}, - {~c"COMPONENT_BASE", ~c"#{component_base}"} - ] + bun_env = if(config[:env] == :dev, do: "development", else: "production") + env = runtime_env("BUN_PORT", bun_port, "BUN_ENV", bun_env) - Port.open( - {:spawn_executable, cmd}, - [ - {:args, args}, - {:cd, cd}, - {:env, env}, - :stream, - :binary, - :exit_status, - # :hide, - :use_stdio, - :stderr_to_stdout - ] - ) + Port.open({:spawn_executable, cmd}, port_options(cmd, args, cd: config[:cd], env: env)) end @impl true def start_file_watcher(component_base) do Logger.debug("Building server.js bundle") - Mix.Tasks.Phx.React.Bun.Bundle.run([ + config = config() + + bundle_args = [ "--component-base", component_base, "--output", - config()[:server_js], + config[:server_js], "--cd", - config()[:cd] - ]) + config[:cd] + ] + + Mix.Tasks.Phx.React.Bun.Bundle.run(bundle_args) Logger.debug("Starting file watcher") Runtime.start_file_watcher(ref: self(), path: component_base) @@ -133,103 +133,61 @@ defmodule Phoenix.React.Runtime.Bun do @impl true def handle_info({:component_base_changed, path}, state) do - Task.async(fn -> - Logger.debug("component_base changed: #{path}, rebuilding...") - - Mix.Tasks.Phx.React.Bun.Bundle.run([ - "--component-base", - state.component_base, - "--output", - state.server_js, - "--cd", - state.cd - ]) - - Logger.debug("component_base rebuilt #{path}") - end) - |> Task.await() + bundle_args = [ + "--component-base", + state.component_base, + "--output", + state.server_js, + "--cd", + state.cd + ] + handle_file_change(path, Mix.Tasks.Phx.React.Bun.Bundle, bundle_args) {:noreply, state} end + @impl true def handle_info({_ref, :ok}, state) do {:noreply, state} end + @impl true def handle_info({_port, {:data, msg}}, state) do Logger.debug(msg) {:noreply, state} end + @impl true def handle_info({port, {:exit_status, exit_status}}, state) do Logger.warning("Bun#{inspect(port)}: exit_status: #{exit_status}") Process.exit(self(), :normal) {:noreply, state} end - # handle the trapped exit call + @impl true def handle_info({:EXIT, _from, reason}, state) do Logger.debug("Bun.Server exiting") - cleanup(reason, state) + cleanup_runtime_process(state.runtime_port, reason) {:stop, reason, state} end + @impl true + def handle_cast(:shutdown, state) do + {:stop, :normal, state} + end + @impl true def get_rendered_component(method, component, props, state) when method in [:render_to_readable_stream, :render_to_string, :render_to_static_markup] do server_port = config()[:port] - - url = ~c"http://localhost:#{server_port}/#{method}/#{component}" - headers = [{~c"Content-Type", ~c"application/json"}] - body = Jason.encode!(props) - timeout = state.render_timeout - case :httpc.request( - :post, - {~c"#{url}", headers, ~c"application/json", body}, - [timeout: timeout, connect_timeout: timeout], - body_format: :binary - ) do - {:ok, {{_version, status_code, _status_text}, _headers, body}} - when status_code in 200..299 -> - {:ok, to_string(body)} - - {:ok, {{_version, status_code, _status_text}, _headers, body}} -> - {:error, "HTTP #{status_code}\n\n#{body}"} - - {:error, reason} -> - {:error, reason} - end + make_http_request(server_port, Atom.to_string(method), component, props, timeout) end @impl true def terminate(reason, state) do Logger.debug("Bun.Server terminating") - cleanup(reason, state) - end - - defp cleanup(reason, %Runtime{runtime_port: runtime_port} = _state) do - case runtime_port |> Port.info(:os_pid) do - {:os_pid, pid} -> - {_, code} = System.cmd("kill", ["-9", "#{pid}"]) - code - - _ -> - 0 - end - - case reason do - :normal -> :normal - :shutdown -> :shutdown - term -> {:shutdown, term} - end - end - - defp get_port_os_pid(runtime_port) do - case runtime_port |> Port.info(:os_pid) do - {:os_pid, pid} -> pid - _ -> nil - end + cleanup_runtime_process(state.runtime_port, reason) end end diff --git a/lib/phoenix/react/runtime/common.ex b/lib/phoenix/react/runtime/common.ex new file mode 100644 index 0000000..f0e6342 --- /dev/null +++ b/lib/phoenix/react/runtime/common.ex @@ -0,0 +1,213 @@ +defmodule Phoenix.React.Runtime.Common do + @moduledoc """ + Common functionality shared between different runtime implementations (Bun, Deno, etc.). + + This module provides shared behavior for: + - Port management and cleanup + - HTTP client operations + - Configuration handling + - Error handling patterns + - Process monitoring + """ + + require Logger + + @doc """ + Standardized error handling for runtime operations. + + Returns {:ok, result} or {:error, reason} tuples consistently. + """ + @spec handle_result(term()) :: {:ok, term()} | {:error, String.t()} + def handle_result({:ok, result}), do: {:ok, result} + def handle_result({:error, reason}), do: {:error, format_error(reason)} + def handle_result(result), do: {:ok, result} + + @doc """ + Formats error messages consistently across runtimes. + """ + @spec format_error(term()) :: String.t() + def format_error(reason) when is_binary(reason), do: reason + def format_error(reason), do: inspect(reason) + + @doc """ + Makes HTTP request to runtime server with standardized error handling. + """ + def make_http_request(server_port, method, component, props, timeout) do + url = "http://localhost:#{server_port}/#{method}/#{component}" + headers = [{~c"Content-Type", ~c"application/json"}] + body = Jason.encode!(props) + + case :httpc.request( + :post, + {String.to_charlist(url), headers, ~c"application/json", String.to_charlist(body)}, + [timeout: timeout, connect_timeout: timeout], + body_format: :binary + ) do + {:ok, {{_version, status_code, _status_text}, _headers, body}} + when status_code in 200..299 -> + {:ok, to_string(body)} + + {:ok, {{_version, status_code, _status_text}, _headers, body}} -> + {:error, "HTTP #{status_code}\n\n#{body}"} + + {:error, reason} -> + {:error, "HTTP request failed: #{format_error(reason)}"} + end + end + + @doc """ + Safely terminates a runtime process with proper cleanup. + """ + @spec cleanup_runtime_process(port(), term()) :: term() + def cleanup_runtime_process(runtime_port, reason) do + case runtime_port |> Port.info(:os_pid) do + {:os_pid, pid} when is_integer(pid) and pid > 0 -> + Logger.debug("Terminating runtime process with PID: #{pid}") + System.cmd("kill", ["-TERM", "#{pid}"]) + + # Give process time to terminate gracefully + Process.sleep(100) + + # Force kill if still running + case System.cmd("kill", ["-0", "#{pid}"]) do + {_, 0} -> + System.cmd("kill", ["-9", "#{pid}"]) + Logger.warning("Force killed runtime process with PID: #{pid}") + + _ -> + :ok + end + + {:os_pid, pid} -> + Logger.warning("Invalid PID found: #{pid}") + + _ -> + Logger.debug("No runtime process to cleanup") + end + + normalize_exit_reason(reason) + end + + @doc """ + Normalizes exit reasons for consistent process termination. + """ + @spec normalize_exit_reason(term()) :: term() + def normalize_exit_reason(:normal), do: :normal + def normalize_exit_reason(:shutdown), do: :shutdown + def normalize_exit_reason({:shutdown, _} = reason), do: reason + def normalize_exit_reason(reason), do: {:shutdown, reason} + + @doc """ + Gets OS PID from port with error handling. + """ + @spec get_port_os_pid(port()) :: integer() | nil + def get_port_os_pid(runtime_port) do + case runtime_port |> Port.info(:os_pid) do + {:os_pid, pid} when is_integer(pid) -> pid + _ -> nil + end + end + + @doc """ + Creates port options for spawning runtime processes. + """ + @spec port_options(String.t(), [String.t()], keyword()) :: keyword() + def port_options(_executable, args, opts \\ []) do + default_opts = [ + {:args, args}, + :stream, + :binary, + :exit_status, + :use_stdio, + :stderr_to_stdout + ] + + custom_opts = + if cd = opts[:cd] do + [{:cd, cd}] + else + [] + end + + env_opts = + if env = opts[:env] do + [{:env, env}] + else + [] + end + + default_opts ++ custom_opts ++ env_opts + end + + @doc """ + Creates environment variables for runtime processes. + """ + @spec runtime_env(String.t(), String.t(), String.t(), String.t()) :: [{charlist(), charlist()}] + def runtime_env(port_var, port_value, env_var, env_value) do + [ + {~c"PORT", String.to_charlist(port_value)}, + {String.to_charlist(port_var), String.to_charlist(port_value)}, + {String.to_charlist(env_var), String.to_charlist(env_value)}, + {~c"COMPONENT_BASE", String.to_charlist(env_value)} + ] + end + + @doc """ + Handles file change events with async processing and timeout. + Uses non-blocking approach to prevent GenServer blocking. + """ + @spec handle_file_change(String.t(), module(), keyword(), timeout()) :: :ok | {:error, term()} + def handle_file_change(path, bundle_module, bundle_args, _timeout \\ 5000) do + Logger.debug("Component base changed: #{path}, rebuilding...") + + # For now, always return :ok since we're handling errors asynchronously + # In a future version, we could implement proper error tracking + Task.start(fn -> + try do + apply(bundle_module, :run, bundle_args) + Logger.debug("Component base rebuilt: #{path}") + rescue + error -> + Logger.error("Failed to rebuild components: #{format_error(error)}") + catch + :throw, error -> + Logger.error("Build failed: #{format_error(error)}") + end + end) + + # Return immediately, don't block the GenServer + :ok + end + + @doc """ + Validates configuration values with proper error messages. + """ + @spec validate_config(keyword(), atom()) :: {:ok, keyword()} | {:error, String.t()} + def validate_config(config, runtime_name) do + cond do + not Keyword.has_key?(config, :cmd) -> + {:error, "#{runtime_name}: :cmd is required in configuration"} + + not Keyword.has_key?(config, :port) -> + {:error, "#{runtime_name}: :port is required in configuration"} + + config[:port] <= 0 or config[:port] > 65535 -> + {:error, "#{runtime_name}: :port must be between 1 and 65535"} + + config[:env] not in [:dev, :prod] -> + {:error, "#{runtime_name}: :env must be :dev or :prod"} + + true -> + {:ok, config} + end + end + + @doc """ + Merges user configuration with defaults, applying validation. + """ + @spec merge_config(keyword(), keyword(), atom()) :: {:ok, keyword()} | {:error, String.t()} + def merge_config(user_config, defaults, runtime_name) do + config = Keyword.merge(defaults, user_config) + validate_config(config, runtime_name) + end +end diff --git a/lib/phoenix/react/runtime/deno.ex b/lib/phoenix/react/runtime/deno.ex new file mode 100644 index 0000000..9b77936 --- /dev/null +++ b/lib/phoenix/react/runtime/deno.ex @@ -0,0 +1,232 @@ +defmodule Phoenix.React.Runtime.Deno do + @moduledoc """ + Phoenix.React.Runtime.Deno + + Config in `runtime.exs` + + ``` + import Config + + config :phoenix_react_server, Phoenix.React.Runtime.Deno, + cd: File.cwd!(), + cmd: "/path/to/deno", + # In dev mode, the server_js will be watched and recompiled when changed + # In prod mode, this need to be precompiled with `mix phx.react.deno.bundle` + server_js: Path.expand("deno/server.js", :code.priv_dir(:phoenix_react_server)), + port: 5226, + env: :dev, + # Security: restrict write access to specific directories + write_dirs: ["/tmp", "/var/tmp"] + ``` + """ + require Logger + + use Phoenix.React.Runtime + import Phoenix.React.Runtime.Common + + def start_link(init_arg) do + GenServer.start_link(__MODULE__, init_arg, name: __MODULE__) + end + + @impl true + def init(component_base: component_base, render_timeout: render_timeout) do + {:ok, + %Runtime{ + component_base: component_base, + render_timeout: render_timeout, + server_js: config()[:server_js], + cd: config()[:cd] + }, {:continue, :start_port}} + end + + @impl true + @spec handle_continue(:start_port, Phoenix.React.Runtime.t()) :: + {:noreply, Phoenix.React.Runtime.t()} + def handle_continue(:start_port, %Runtime{component_base: component_base} = state) do + if config()[:env] == :dev do + start_file_watcher(component_base) + Phoenix.React.Runtime.FileWatcher.set_ref(self()) + end + + port = start(component_base: component_base) + + Logger.debug( + "Deno.Server started on port: #{inspect(port)} and OS pid: #{get_port_os_pid(port)}" + ) + + Phoenix.React.Monitoring.record_runtime_startup("Deno", config()[:port]) + + Phoenix.React.Server.set_runtime_process(self()) + + {:noreply, %Runtime{state | runtime_port: port}} + end + + @impl true + def config() do + user_config = Application.get_env(:phoenix_react_server, Phoenix.React.Runtime.Deno, []) + + # Convert user config to map for new config system + user_config_map = + user_config + |> Enum.into(%{}) + |> Map.put(:cd, Keyword.get(user_config, :cd, File.cwd!())) + |> Map.put(:cmd, Keyword.get(user_config, :cmd, System.find_executable("deno"))) + |> Map.put( + :server_js, + Keyword.get( + user_config, + :server_js, + Path.expand("deno/server.js", :code.priv_dir(:phoenix_react_server)) + ) + ) + + case Phoenix.React.Config.runtime_config(:deno, user_config_map) do + {:ok, config} -> Phoenix.React.Config.to_keyword_list(config) + {:error, reason} -> raise ArgumentError, reason + end + end + + @impl true + def start(component_base: _component_base) do + config = config() + cmd = config[:cmd] + server_js = config[:server_js] + deno_port = Integer.to_string(config[:port]) + + is_dev = config[:env] == :dev + deno_env = if(is_dev, do: "development", else: "production") + + # In development, use the source file with deno run + # In production, use the compiled binary directly + {exec_cmd, args} = + if is_dev do + # Use source file in development + source_js = Path.join(Path.dirname(server_js), "server_source.js") + + # Security: restrict write access to specific directories + write_dirs = config[:write_dirs] || ["/tmp", "/var/tmp"] + write_args = Enum.flat_map(write_dirs, &["--allow-write=#{&1}"]) + + args = + [ + "run", + "--allow-net", + "--allow-read", + "--allow-env" + ] ++ + write_args ++ + [ + "--watch", + "--node-modules-dir" + ] + + {cmd, args ++ [source_js]} + else + # In production, server_js is the compiled binary + {server_js, []} + end + + env = + runtime_env("DENO_PORT", deno_port, "DENO_ENV", deno_env) ++ + [{~c"PARENT_CHECK_INTERVAL", ~c"#{config[:parent_check_interval] || 5000}"}] + + Port.open( + {:spawn_executable, exec_cmd}, + port_options(exec_cmd, args, cd: config[:cd], env: env) + ) + end + + @impl true + def start_file_watcher(component_base) do + Logger.debug("Building server.js bundle") + + config = config() + source_js = Path.join(Path.dirname(config[:server_js]), "server_source.js") + + bundle_args = [ + "--component-base", + component_base, + "--output", + source_js, + "--cd", + config[:cd] + ] + + Mix.Tasks.Phx.React.Deno.Bundle.run(bundle_args) + + Logger.debug("Starting file watcher") + Runtime.start_file_watcher(ref: self(), path: component_base) + end + + @impl true + def handle_info({:component_base_changed, path}, state) do + source_js = Path.join(Path.dirname(state.server_js), "server_source.js") + + bundle_args = [ + "--component-base", + state.component_base, + "--output", + source_js, + "--cd", + state.cd + ] + + handle_file_change(path, Mix.Tasks.Phx.React.Deno.Bundle, bundle_args) + {:noreply, state} + end + + @impl true + def handle_info({_ref, :ok}, state) do + {:noreply, state} + end + + @impl true + def handle_info({_port, {:data, msg}}, state) do + Logger.debug(msg) + {:noreply, state} + end + + @impl true + def handle_info({port, {:exit_status, exit_status}}, state) do + Logger.warning("Deno#{inspect(port)}: exit_status: #{exit_status}") + Process.exit(self(), :normal) + {:noreply, state} + end + + @impl true + def handle_info({:EXIT, _from, reason}, state) do + Logger.debug("Deno.Server exiting") + cleanup_runtime_process(state.runtime_port, reason) + {:stop, reason, state} + end + + @impl true + def get_rendered_component(method, component, props, state) + when method in [:render_to_readable_stream, :render_to_string, :render_to_static_markup] do + server_port = config()[:port] + timeout = state.render_timeout + + Phoenix.React.Monitoring.measure( + "render_#{method}_#{component}", + [:phoenix, :react, :render], + fn -> + result = make_http_request(server_port, Atom.to_string(method), component, props, timeout) + + # Record the result for monitoring + case result do + {:ok, _} -> Phoenix.React.Monitoring.record_render(component, method, 0, :ok) + {:error, _} -> Phoenix.React.Monitoring.record_render(component, method, 0, :error) + end + + result + end + ) + end + + @impl true + def terminate(reason, state) do + Logger.debug("Deno.Server terminating") + Phoenix.React.Monitoring.record_runtime_shutdown("Deno", reason) + cleanup_runtime_process(state.runtime_port, reason) + end +end diff --git a/lib/phoenix/react/runtime/file_watcher.ex b/lib/phoenix/react/runtime/file_watcher.ex index 48ffa99..393e238 100644 --- a/lib/phoenix/react/runtime/file_watcher.ex +++ b/lib/phoenix/react/runtime/file_watcher.ex @@ -15,14 +15,29 @@ defmodule Phoenix.React.Runtime.FileWatcher do @impl true def init(args) do path = Keyword.fetch!(args, :path) - {:ok, watcher_pid} = FileSystem.start_link(dirs: [path]) - FileSystem.subscribe(watcher_pid) - IO.puts("Watching #{path} for changes...") - {:ok, - args - |> Keyword.put(:watcher_pid, watcher_pid) - |> Keyword.put(:update_time, System.os_time(:second))} + case FileSystem.start_link(dirs: [path]) do + {:ok, watcher_pid} -> + FileSystem.subscribe(watcher_pid) + IO.puts("Watching #{path} for changes...") + + {:ok, + args + |> Keyword.put(:watcher_pid, watcher_pid) + |> Keyword.put(:update_time, System.os_time(:second))} + + :ignore -> + Logger.warning("File system watcher not available (inotify-tools missing)") + + {:ok, + args + |> Keyword.put(:watcher_pid, nil) + |> Keyword.put(:update_time, System.os_time(:second))} + + {:error, reason} -> + Logger.error("Failed to start file system watcher: #{inspect(reason)}") + {:stop, reason} + end end @impl true diff --git a/priv/deno/jsx_components/markdown.jsx b/priv/deno/jsx_components/markdown.jsx new file mode 100644 index 0000000..25fe8bd --- /dev/null +++ b/priv/deno/jsx_components/markdown.jsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import Markdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +export const Component = (props = {}) => { + return ( + + {children} + + ) + } + }} + > + {props.data} + + ); +} diff --git a/priv/deno/jsx_components/tab.jsx b/priv/deno/jsx_components/tab.jsx new file mode 100644 index 0000000..03f1995 --- /dev/null +++ b/priv/deno/jsx_components/tab.jsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +export const Component = ({ tabs }) => { + let list = tabs ?? []; + return ( +
+ {list.map((tab, index) => ( +
+ {tab} +
+ ))} +
+ ) +} diff --git a/priv/deno/server_source.js b/priv/deno/server_source.js new file mode 100644 index 0000000..2898caa --- /dev/null +++ b/priv/deno/server_source.js @@ -0,0 +1,187 @@ +import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; +import React from "npm:react"; +import { renderToReadableStream, renderToString, renderToStaticMarkup } from "npm:react-dom/server"; + +const __comMap = {}; + +import { Component as __component_0 } from "./jsx_components/markdown.jsx"; +__comMap["markdown"] = __component_0; + +import { Component as __component_1 } from "./jsx_components/tab.jsx"; +__comMap["tab"] = __component_1; + + +const { COMPONENT_BASE, DENO_ENV } = Deno.env.toObject(); + +const isDev = DENO_ENV === 'development'; + +const port = parseInt(Deno.env.get("PORT") || "5226"); + +const handler = async (req) => { + try { + let bodyStream = req.body; + if (isDev) { + const bodyText = await req.text(); + console.log('Request: ', req.method, req.url, bodyText); + bodyStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(bodyText)); + controller.close(); + } + }); + } + const { url } = req; + const uri = new URL(url); + const { pathname } = uri; + + // Security: Validate pathname to prevent path traversal + if (pathname.includes('..') || pathname.includes('\\')) { + return new Response('Invalid path', { status: 400 }); + } + + if (pathname.startsWith('/stop')) { + return new Response('{"message":"ok"}', { + headers: { + "Content-Type": "application/json", + }, + }); + } + + if (pathname.startsWith('/render_to_static_markup/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_static_markup\//, ''); + + // Security: Validate component name + if (!/^[a-zA-Z0-9_-]+$/.test(fileName)) { + return new Response('Invalid component name', { status: 400 }); + } + + const Component = __comMap[fileName]; + if (!Component) { + return new Response(`Not Found, component not found.`, { + status: 404, + headers: { + "Content-Type": "text/html", + }, + }); + } + const jsxNode = React.createElement(Component, props); + const html = renderToStaticMarkup(jsxNode); + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + if (pathname.startsWith('/render_to_string/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_string\//, ''); + + // Security: Validate component name + if (!/^[a-zA-Z0-9_-]+$/.test(fileName)) { + return new Response('Invalid component name', { status: 400 }); + } + + const Component = __comMap[fileName]; + const jsxNode = React.createElement(Component, props); + const html = renderToString(jsxNode); + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + if (pathname.startsWith('/render_to_readable_stream/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_readable_stream\//, ''); + + // Security: Validate component name + if (!/^[a-zA-Z0-9_-]+$/.test(fileName)) { + return new Response('Invalid component name', { status: 400 }); + } + + const Component = __comMap[fileName]; + const jsxNode = React.createElement(Component, props); + const stream = await renderToReadableStream(jsxNode); + return new Response(stream, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + return new Response(`Not Found, not matched request.`, { + status: 404, + headers: { + "Content-Type": "text/html", + }, + }); + } catch(error) { + const html = ` + + `; + return new Response(html, { + status: 500, + headers: { + "Content-Type": "text/html", + }, + }); + } +}; + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +console.log(`Server started at http://localhost:${port}`); +console.log(`COMPONENT_BASE`, COMPONENT_BASE); +console.log(`DENO_ENV`, DENO_ENV); + +const ppid = Deno.pid; +const parentCheckInterval = parseInt(Deno.env.get("PARENT_CHECK_INTERVAL") || "5000"); + +const checkParentInterval = setInterval(() => { + try { + // Enhanced validation: ensure PPID is valid and different from current process + if (ppid && ppid > 1 && ppid !== Deno.pid) { + Deno.kill(ppid, "0"); + } else { + console.log("Invalid PPID detected. Shutting down server..."); + clearInterval(checkParentInterval); + Deno.exit(0); + } + } catch (e) { + console.log("Parent process exited. Shutting down server..."); + clearInterval(checkParentInterval); + Deno.exit(0); + } +}, parentCheckInterval); + +const shutdown = async (signal) => { + console.log(`\nReceived ${signal}. Cleaning up...`); + clearInterval(checkParentInterval); + console.log("Cleanup done. Exiting."); + Deno.exit(0); +}; + +Deno.addSignalListener("SIGINT", () => { + shutdown("SIGINT"); +}); + +Deno.addSignalListener("SIGTERM", () => { + shutdown("SIGTERM"); +}); + +await serve(handler, { port }); \ No newline at end of file diff --git a/react_demo/README.md b/react_demo/README.md index a8e2db5..536d7fb 100644 --- a/react_demo/README.md +++ b/react_demo/README.md @@ -1,13 +1,79 @@ # ReactDemo +A Phoenix application demonstrating React server-side rendering with support for both Bun and Deno runtimes. + +## Getting Started + To start your Phoenix server: * Run `mix setup` to install and setup dependencies * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. +Now you can visit [`localhost:4666`](http://localhost:4666) from your browser. + +## React Runtime Options + +This demo supports both Bun and Deno runtimes for React server-side rendering. + +### Using Bun (Default) + +```bash +mix phx.server +# or +REACT_RUNTIME=bun mix phx.server +``` + +### Using Deno + +```bash +REACT_RUNTIME=deno mix phx.server +``` + +### Building for Production + +#### Bun Production Build + +```bash +# Build React components +mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/bun/server.js + +# Start production server +MIX_ENV=prod mix phx.server +# or +REACT_RUNTIME=bun MIX_ENV=prod mix phx.server +``` + +#### Deno Production Build + +```bash +# Build React components +mix phx.react.deno.bundle --component-base=assets/component --output=priv/react/deno/server.js + +# Start production server +REACT_RUNTIME=deno MIX_ENV=prod mix phx.server +``` + +## Configuration + +The React runtime can be configured via the `REACT_RUNTIME` environment variable: + +- `REACT_RUNTIME=bun` (default) - Uses Bun runtime +- `REACT_RUNTIME=deno` - Uses Deno runtime + +Runtime-specific configurations are in the config files: + +- **Development**: `config/dev.exs` +- **Production**: `config/prod.exs` +- **Runtime**: `config/runtime.exs` + +## Features -Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). +- [x] Server-side React rendering +- [x] Multiple runtime support (Bun & Deno) +- [x] Hot reload in development +- [x] LiveView integration +- [x] Component caching +- [x] Production bundling ## Learn more @@ -16,3 +82,4 @@ Ready to run in production? Please [check our deployment guides](https://hexdocs * Docs: https://hexdocs.pm/phoenix * Forum: https://elixirforum.com/c/phoenix-forum * Source: https://github.com/phoenixframework/phoenix + * Phoenix.React: https://hexdocs.pm/phoenix_react_server/ diff --git a/react_demo/assets/component/markdown.js b/react_demo/assets/component/markdown.js index fe3c540..df7b594 100644 --- a/react_demo/assets/component/markdown.js +++ b/react_demo/assets/component/markdown.js @@ -4,7 +4,7 @@ import remarkGfm from 'remark-gfm'; import MarkdownPreview from '@uiw/react-markdown-preview'; import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'; -import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism'; +import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism/index.js'; export const Component = (props = {}) => { diff --git a/react_demo/build.sh b/react_demo/build.sh new file mode 100755 index 0000000..619518d --- /dev/null +++ b/react_demo/build.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# ReactDemo Production Build Script +# Usage: ./build.sh [bun|deno] + +RUNTIME=${1:-bun} + +echo "🏗️ Building ReactDemo for production with $RUNTIME runtime..." +echo "" + +# Set the runtime environment variable +export REACT_RUNTIME=$RUNTIME + +case $RUNTIME in + "bun") + echo "📦 Building with Bun runtime..." + mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/bun/server.js + ;; + "deno") + echo "🦕 Building with Deno runtime..." + mix phx.react.deno.bundle --component-base=assets/component --output=priv/react/deno/server.js + ;; + *) + echo "❌ Invalid runtime. Use 'bun' or 'deno'" + exit 1 + ;; +esac + +echo "" +echo "✅ Build completed!" +echo "🚀 Start production server with: REACT_RUNTIME=$RUNTIME MIX_ENV=prod mix phx.server" \ No newline at end of file diff --git a/react_demo/config/config.exs b/react_demo/config/config.exs index a97ace4..85887a8 100644 --- a/react_demo/config/config.exs +++ b/react_demo/config/config.exs @@ -51,8 +51,17 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason +# Configure Phoenix.React server runtime +# Use environment variable REACT_RUNTIME to switch between :bun and :deno +runtime = + case System.get_env("REACT_RUNTIME", "bun") do + "bun" -> Phoenix.React.Runtime.Bun + "deno" -> Phoenix.React.Runtime.Deno + _ -> Phoenix.React.Runtime.Bun + end + config :phoenix_react_server, Phoenix.React, - runtime: Phoenix.React.Runtime.Bun, + runtime: runtime, component_base: Path.expand("../assets/component", __DIR__), cache_ttl: 60 diff --git a/react_demo/config/dev.exs b/react_demo/config/dev.exs index c6816f0..8441590 100644 --- a/react_demo/config/dev.exs +++ b/react_demo/config/dev.exs @@ -30,13 +30,32 @@ config :phoenix_live_view, debug_heex_annotations: true, enable_expensive_runtime_checks: true +# Configure Phoenix.React server for development +# Use environment variable REACT_RUNTIME to switch between :bun and :deno +runtime = + case System.get_env("REACT_RUNTIME", "bun") do + "bun" -> Phoenix.React.Runtime.Bun + "deno" -> Phoenix.React.Runtime.Deno + _ -> Phoenix.React.Runtime.Bun + end + config :phoenix_react_server, Phoenix.React, - runtime: Phoenix.React.Runtime.Bun, + runtime: runtime, component_base: Path.expand("../assets/component", __DIR__), cache_ttl: 10 +# Bun configuration config :phoenix_react_server, Phoenix.React.Runtime.Bun, cd: Path.expand("..", __DIR__), cmd: System.find_executable("bun"), + server_js: Path.expand("../priv/react/bun/server.js", __DIR__), + port: 5124, + env: :dev + +# Deno configuration +config :phoenix_react_server, Phoenix.React.Runtime.Deno, + cd: Path.expand("..", __DIR__), + cmd: System.find_executable("deno"), server_js: Path.expand("../priv/react/server.js", __DIR__), + port: 5125, env: :dev diff --git a/react_demo/config/prod.exs b/react_demo/config/prod.exs index 92f87b5..4503769 100644 --- a/react_demo/config/prod.exs +++ b/react_demo/config/prod.exs @@ -14,11 +14,30 @@ config :react_demo, ReactDemoWeb.Endpoint, config :react_demo, ReactDemoWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" +# Configure Phoenix.React server for production +# Use environment variable REACT_RUNTIME to switch between :bun and :deno +runtime = + case System.get_env("REACT_RUNTIME", "bun") do + "bun" -> Phoenix.React.Runtime.Bun + "deno" -> Phoenix.React.Runtime.Deno + _ -> Phoenix.React.Runtime.Bun + end + +config :phoenix_react_server, Phoenix.React, runtime: runtime + +# Bun configuration for production config :phoenix_react_server, Phoenix.React.Runtime.Bun, cmd: System.find_executable("bun"), - server_js: Path.expand("../priv/react/server.js", __DIR__), + server_js: Path.expand("../priv/react/bun/server.js", __DIR__), port: 5124, - env: :dev + env: :prod + +# Deno configuration for production +config :phoenix_react_server, Phoenix.React.Runtime.Deno, + cmd: System.find_executable("deno"), + server_js: Path.expand("../priv/react/server.js", __DIR__), + port: 5125, + env: :prod # Do not print debug messages in production config :logger, :console, format: "[$level] $message\n" diff --git a/react_demo/config/runtime.exs b/react_demo/config/runtime.exs index 19d9240..fa6a771 100644 --- a/react_demo/config/runtime.exs +++ b/react_demo/config/runtime.exs @@ -15,3 +15,28 @@ if config_env() == :prod do port: port ] end + +# Configure Phoenix.React server for OTP releases +# Use environment variable REACT_RUNTIME to switch between :bun and :deno +runtime = + case System.get_env("REACT_RUNTIME", "bun") do + "bun" -> Phoenix.React.Runtime.Bun + "deno" -> Phoenix.React.Runtime.Deno + _ -> Phoenix.React.Runtime.Bun + end + +config :phoenix_react_server, Phoenix.React, runtime: runtime + +# Bun configuration for releases +config :phoenix_react_server, Phoenix.React.Runtime.Bun, + cmd: System.find_executable("bun"), + server_js: Path.expand("../priv/react/bun/server.js", __DIR__), + port: 5124, + env: :prod + +# Deno configuration for releases +config :phoenix_react_server, Phoenix.React.Runtime.Deno, + cmd: System.find_executable("deno"), + server_js: Path.expand("../priv/react/server.js", __DIR__), + port: 5125, + env: if(config_env() == :prod, do: :prod, else: :dev) diff --git a/react_demo/deno.lock b/react_demo/deno.lock new file mode 100644 index 0000000..ff3b1ac --- /dev/null +++ b/react_demo/deno.lock @@ -0,0 +1,2967 @@ +{ + "version": "4", + "specifiers": { + "npm:@emotion/react@^11.14.0": "11.14.0_react@19.2.0", + "npm:@emotion/server@^11.11.0": "11.11.0", + "npm:@emotion/styled@^11.14.0": "11.14.1_@emotion+react@11.14.0__react@19.2.0_react@19.2.0", + "npm:@headlessui/react@^2.2.0": "2.2.9_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:@mui/material@^6.4.3": "6.5.0_@emotion+react@11.14.0__react@19.2.0_@emotion+styled@11.14.1__@emotion+react@11.14.0___react@19.2.0__react@19.2.0_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:@mui/x-charts@^7.26.0": "7.29.1_@emotion+react@11.14.0__react@19.2.0_@emotion+styled@11.14.1__@emotion+react@11.14.0___react@19.2.0__react@19.2.0_@mui+material@6.5.0__@emotion+react@11.14.0___react@19.2.0__@emotion+styled@11.14.1___@emotion+react@11.14.0____react@19.2.0___react@19.2.0__react@19.2.0__react-dom@19.2.0___react@19.2.0_@mui+system@6.5.0__@emotion+react@11.14.0___react@19.2.0__@emotion+styled@11.14.1___@emotion+react@11.14.0____react@19.2.0___react@19.2.0__react@19.2.0_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:@nivo/line@0.88": "0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:@tailwindcss/typography@~0.5.16": "0.5.19_tailwindcss@4.1.14", + "npm:@uiw/react-markdown-preview@^5.1.3": "5.1.5_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:@uiw/react-md-editor@^4.0.5": "4.0.8_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:@visx/xychart@^3.12.0": "3.12.0_@react-spring+web@9.7.5__react@19.2.0__react-dom@19.2.0___react@19.2.0_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:d3@^7.9.0": "7.9.0_d3-selection@3.0.0", + "npm:daisyui@^4.12.23": "4.12.24", + "npm:date-fns@^4.1.0": "4.1.0", + "npm:react-dom@*": "19.2.0_react@19.2.0", + "npm:react-dom@19": "19.2.0_react@19.2.0", + "npm:react-markdown@^9.0.3": "9.1.0_@types+react@19.2.2_react@19.2.0", + "npm:react-syntax-highlighter@^15.6.1": "15.6.6_react@19.2.0", + "npm:react@*": "19.2.0", + "npm:react@19": "19.2.0", + "npm:recharts@^2.15.1": "2.15.4_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:remark-gfm@4": "4.0.1" + }, + "npm": { + "@babel/code-frame@7.27.1": { + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": [ + "@babel/helper-validator-identifier", + "js-tokens", + "picocolors" + ] + }, + "@babel/generator@7.28.3": { + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dependencies": [ + "@babel/parser", + "@babel/types", + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping", + "jsesc" + ] + }, + "@babel/helper-globals@7.28.0": { + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" + }, + "@babel/helper-module-imports@7.27.1": { + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dependencies": [ + "@babel/traverse", + "@babel/types" + ] + }, + "@babel/helper-string-parser@7.27.1": { + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier@7.27.1": { + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + }, + "@babel/parser@7.28.4": { + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dependencies": [ + "@babel/types" + ] + }, + "@babel/runtime@7.28.4": { + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==" + }, + "@babel/template@7.27.2": { + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dependencies": [ + "@babel/code-frame", + "@babel/parser", + "@babel/types" + ] + }, + "@babel/traverse@7.28.4": { + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dependencies": [ + "@babel/code-frame", + "@babel/generator", + "@babel/helper-globals", + "@babel/parser", + "@babel/template", + "@babel/types", + "debug" + ] + }, + "@babel/types@7.28.4": { + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dependencies": [ + "@babel/helper-string-parser", + "@babel/helper-validator-identifier" + ] + }, + "@emotion/babel-plugin@11.13.5": { + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dependencies": [ + "@babel/helper-module-imports", + "@babel/runtime", + "@emotion/hash", + "@emotion/memoize", + "@emotion/serialize", + "babel-plugin-macros", + "convert-source-map", + "escape-string-regexp@4.0.0", + "find-root", + "source-map", + "stylis" + ] + }, + "@emotion/cache@11.14.0": { + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dependencies": [ + "@emotion/memoize", + "@emotion/sheet", + "@emotion/utils", + "@emotion/weak-memoize", + "stylis" + ] + }, + "@emotion/hash@0.9.2": { + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "@emotion/is-prop-valid@1.4.0": { + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "dependencies": [ + "@emotion/memoize" + ] + }, + "@emotion/memoize@0.9.0": { + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "@emotion/react@11.14.0_react@19.2.0": { + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dependencies": [ + "@babel/runtime", + "@emotion/babel-plugin", + "@emotion/cache", + "@emotion/serialize", + "@emotion/use-insertion-effect-with-fallbacks", + "@emotion/utils", + "@emotion/weak-memoize", + "hoist-non-react-statics", + "react" + ] + }, + "@emotion/serialize@1.3.3": { + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dependencies": [ + "@emotion/hash", + "@emotion/memoize", + "@emotion/unitless", + "@emotion/utils", + "csstype" + ] + }, + "@emotion/server@11.11.0": { + "integrity": "sha512-6q89fj2z8VBTx9w93kJ5n51hsmtYuFPtZgnc1L8VzRx9ti4EU6EyvF6Nn1H1x3vcCQCF7u2dB2lY4AYJwUW4PA==", + "dependencies": [ + "@emotion/utils", + "html-tokenize", + "multipipe", + "through" + ] + }, + "@emotion/sheet@1.4.0": { + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "@emotion/styled@11.14.1_@emotion+react@11.14.0__react@19.2.0_react@19.2.0": { + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "dependencies": [ + "@babel/runtime", + "@emotion/babel-plugin", + "@emotion/is-prop-valid", + "@emotion/react", + "@emotion/serialize", + "@emotion/use-insertion-effect-with-fallbacks", + "@emotion/utils", + "react" + ] + }, + "@emotion/unitless@0.10.0": { + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "@emotion/use-insertion-effect-with-fallbacks@1.2.0_react@19.2.0": { + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "dependencies": [ + "react" + ] + }, + "@emotion/utils@1.4.2": { + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "@emotion/weak-memoize@0.4.0": { + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "@floating-ui/core@1.7.3": { + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dependencies": [ + "@floating-ui/utils" + ] + }, + "@floating-ui/dom@1.7.4": { + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dependencies": [ + "@floating-ui/core", + "@floating-ui/utils" + ] + }, + "@floating-ui/react-dom@2.1.6_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dependencies": [ + "@floating-ui/dom", + "react", + "react-dom" + ] + }, + "@floating-ui/react@0.26.28_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "dependencies": [ + "@floating-ui/react-dom", + "@floating-ui/utils", + "react", + "react-dom", + "tabbable" + ] + }, + "@floating-ui/utils@0.2.10": { + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "@headlessui/react@2.2.9_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", + "dependencies": [ + "@floating-ui/react", + "@react-aria/focus", + "@react-aria/interactions", + "@tanstack/react-virtual", + "react", + "react-dom", + "use-sync-external-store" + ] + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.5.5": { + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping@0.3.31": { + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@mui/core-downloads-tracker@6.5.0": { + "integrity": "sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==" + }, + "@mui/material@6.5.0_@emotion+react@11.14.0__react@19.2.0_@emotion+styled@11.14.1__@emotion+react@11.14.0___react@19.2.0__react@19.2.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==", + "dependencies": [ + "@babel/runtime", + "@emotion/react", + "@emotion/styled", + "@mui/core-downloads-tracker", + "@mui/system", + "@mui/types", + "@mui/utils", + "@popperjs/core", + "@types/react-transition-group", + "clsx", + "csstype", + "prop-types", + "react", + "react-dom", + "react-is@19.2.0", + "react-transition-group" + ] + }, + "@mui/private-theming@6.4.9_react@19.2.0": { + "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", + "dependencies": [ + "@babel/runtime", + "@mui/utils", + "prop-types", + "react" + ] + }, + "@mui/styled-engine@6.5.0_@emotion+react@11.14.0__react@19.2.0_@emotion+styled@11.14.1__@emotion+react@11.14.0___react@19.2.0__react@19.2.0_react@19.2.0": { + "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", + "dependencies": [ + "@babel/runtime", + "@emotion/cache", + "@emotion/react", + "@emotion/serialize", + "@emotion/sheet", + "@emotion/styled", + "csstype", + "prop-types", + "react" + ] + }, + "@mui/system@6.5.0_@emotion+react@11.14.0__react@19.2.0_@emotion+styled@11.14.1__@emotion+react@11.14.0___react@19.2.0__react@19.2.0_react@19.2.0": { + "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", + "dependencies": [ + "@babel/runtime", + "@emotion/react", + "@emotion/styled", + "@mui/private-theming", + "@mui/styled-engine", + "@mui/types", + "@mui/utils", + "clsx", + "csstype", + "prop-types", + "react" + ] + }, + "@mui/types@7.2.24": { + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==" + }, + "@mui/utils@6.4.9_react@19.2.0": { + "integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==", + "dependencies": [ + "@babel/runtime", + "@mui/types", + "@types/prop-types", + "clsx", + "prop-types", + "react", + "react-is@19.2.0" + ] + }, + "@mui/x-charts-vendor@7.20.0": { + "integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==", + "dependencies": [ + "@babel/runtime", + "@types/d3-color@3.1.3", + "@types/d3-delaunay@6.0.4", + "@types/d3-interpolate@3.0.4", + "@types/d3-scale@4.0.9", + "@types/d3-shape@3.1.7", + "@types/d3-time@3.0.4", + "d3-color", + "d3-delaunay@6.0.4", + "d3-interpolate", + "d3-scale", + "d3-shape@3.2.0", + "d3-time@3.1.0", + "delaunator", + "robust-predicates" + ] + }, + "@mui/x-charts@7.29.1_@emotion+react@11.14.0__react@19.2.0_@emotion+styled@11.14.1__@emotion+react@11.14.0___react@19.2.0__react@19.2.0_@mui+material@6.5.0__@emotion+react@11.14.0___react@19.2.0__@emotion+styled@11.14.1___@emotion+react@11.14.0____react@19.2.0___react@19.2.0__react@19.2.0__react-dom@19.2.0___react@19.2.0_@mui+system@6.5.0__@emotion+react@11.14.0___react@19.2.0__@emotion+styled@11.14.1___@emotion+react@11.14.0____react@19.2.0___react@19.2.0__react@19.2.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-5s9PX51HWhpMa+DCDa4RgjtODSaMe+PlTZUqoGIil2vaW/+4ouDLREXvyuVvIF93KfZwrPKAL2SJKSQS4YYB2w==", + "dependencies": [ + "@babel/runtime", + "@emotion/react", + "@emotion/styled", + "@mui/material", + "@mui/system", + "@mui/utils", + "@mui/x-charts-vendor", + "@mui/x-internals", + "@react-spring/rafz", + "@react-spring/web", + "clsx", + "prop-types", + "react", + "react-dom" + ] + }, + "@mui/x-internals@7.29.0_react@19.2.0": { + "integrity": "sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==", + "dependencies": [ + "@babel/runtime", + "@mui/utils", + "react" + ] + }, + "@nivo/annotations@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-NXE+1oIUn+EGWMQpnpeRMLgi2wyuzhGDoJQY4OUHissCUiNotid2oNQ/PXJwN0toiu+/j9SyhzI32xr70OPi7Q==", + "dependencies": [ + "@nivo/colors", + "@nivo/core", + "@react-spring/web", + "lodash", + "react" + ] + }, + "@nivo/axes@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-jF7aIxzTNayV5cI1J/b9Q1FfpMBxTXGk3OwSigXMSfYWlliskDn2u0qGRLiYhuXFdQAWIp4oXsO1GcAQ0eRVdg==", + "dependencies": [ + "@nivo/core", + "@nivo/scales", + "@react-spring/web", + "@types/d3-format@1.4.5", + "@types/d3-time-format@2.3.4", + "d3-format@1.4.5", + "d3-time-format@3.0.0", + "react" + ] + }, + "@nivo/colors@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-IZ+leYIqAlo7dyLHmsQwujanfRgXyoQ5H7PU3RWLEn1PP0zxDKLgEjFEDADpDauuslh2Tx0L81GNkWR6QSP0Mw==", + "dependencies": [ + "@nivo/core", + "@types/d3-color@3.1.3", + "@types/d3-scale-chromatic", + "@types/d3-scale@4.0.9", + "@types/prop-types", + "d3-color", + "d3-scale", + "d3-scale-chromatic", + "lodash", + "prop-types", + "react" + ] + }, + "@nivo/core@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-XjUkA5MmwjLP38bdrJwn36Gj7T5SYMKD55LYQp/1nIJPdxqJ38dUfE4XyBDfIEgfP6yrHOihw3C63cUdnUBoiw==", + "dependencies": [ + "@nivo/tooltip", + "@react-spring/web", + "@types/d3-shape@3.1.7", + "d3-color", + "d3-format@1.4.5", + "d3-interpolate", + "d3-scale", + "d3-scale-chromatic", + "d3-shape@3.2.0", + "d3-time-format@3.0.0", + "lodash", + "prop-types", + "react" + ] + }, + "@nivo/legends@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-d4DF9pHbD8LmGJlp/Gp1cF4e8y2wfQTcw3jVhbZj9zkb7ZWB7JfeF60VHRfbXNux9bjQ9U78/SssQqueVDPEmg==", + "dependencies": [ + "@nivo/colors", + "@nivo/core", + "@types/d3-scale@4.0.9", + "d3-scale", + "react" + ] + }, + "@nivo/line@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-hFTyZ3BdAZvq2HwdwMj2SJGUeodjEW+7DLtFMIIoVIxmjZlAs3z533HcJ9cJd3it928fDm8SF/rgHs0TztYf9Q==", + "dependencies": [ + "@nivo/annotations", + "@nivo/axes", + "@nivo/colors", + "@nivo/core", + "@nivo/legends", + "@nivo/scales", + "@nivo/tooltip", + "@nivo/voronoi", + "@react-spring/web", + "d3-shape@3.2.0", + "react" + ] + }, + "@nivo/scales@0.88.0": { + "integrity": "sha512-HbpxkQp6tHCltZ1yDGeqdLcaJl5ze54NPjurfGtx/Uq+H5IQoBd4Tln49bUar5CsFAMsXw8yF1HQvASr7I1SIA==", + "dependencies": [ + "@types/d3-scale@4.0.9", + "@types/d3-time-format@3.0.4", + "@types/d3-time@1.1.4", + "d3-scale", + "d3-time-format@3.0.0", + "d3-time@1.1.0", + "lodash" + ] + }, + "@nivo/tooltip@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-iEjVfQA8gumAzg/yUinjTwswygCkE5Iwuo8opwnrbpNIqMrleBV+EAKIgB0PrzepIoW8CFG/SJhoiRfbU8jhOw==", + "dependencies": [ + "@nivo/core", + "@react-spring/web", + "react" + ] + }, + "@nivo/voronoi@0.88.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-MyiNLvODthFoMjQ7Wjp693nogbTmVEx8Yn/7QkJhyPQbFyyA37TF/D1a/ox4h2OslXtP6K9QFN+42gB/zu7ixw==", + "dependencies": [ + "@nivo/core", + "@nivo/tooltip", + "@types/d3-delaunay@6.0.4", + "@types/d3-scale@4.0.9", + "d3-delaunay@6.0.4", + "d3-scale", + "react" + ] + }, + "@popperjs/core@2.11.8": { + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + }, + "@react-aria/focus@3.21.2_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", + "dependencies": [ + "@react-aria/interactions", + "@react-aria/utils", + "@react-types/shared", + "@swc/helpers", + "clsx", + "react", + "react-dom" + ] + }, + "@react-aria/interactions@3.25.6_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "dependencies": [ + "@react-aria/ssr", + "@react-aria/utils", + "@react-stately/flags", + "@react-types/shared", + "@swc/helpers", + "react", + "react-dom" + ] + }, + "@react-aria/ssr@3.9.10_react@19.2.0": { + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "dependencies": [ + "@swc/helpers", + "react" + ] + }, + "@react-aria/utils@3.31.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", + "dependencies": [ + "@react-aria/ssr", + "@react-stately/flags", + "@react-stately/utils", + "@react-types/shared", + "@swc/helpers", + "clsx", + "react", + "react-dom" + ] + }, + "@react-spring/animated@9.7.5_react@19.2.0": { + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "dependencies": [ + "@react-spring/shared", + "@react-spring/types", + "react" + ] + }, + "@react-spring/core@9.7.5_react@19.2.0": { + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "dependencies": [ + "@react-spring/animated", + "@react-spring/shared", + "@react-spring/types", + "react" + ] + }, + "@react-spring/rafz@9.7.5": { + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==" + }, + "@react-spring/shared@9.7.5_react@19.2.0": { + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "dependencies": [ + "@react-spring/rafz", + "@react-spring/types", + "react" + ] + }, + "@react-spring/types@9.7.5": { + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==" + }, + "@react-spring/web@9.7.5_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "dependencies": [ + "@react-spring/animated", + "@react-spring/core", + "@react-spring/shared", + "@react-spring/types", + "react", + "react-dom" + ] + }, + "@react-stately/flags@3.1.2": { + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "dependencies": [ + "@swc/helpers" + ] + }, + "@react-stately/utils@3.10.8_react@19.2.0": { + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "dependencies": [ + "@swc/helpers", + "react" + ] + }, + "@react-types/shared@3.32.1_react@19.2.0": { + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "dependencies": [ + "react" + ] + }, + "@swc/helpers@0.5.17": { + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dependencies": [ + "tslib" + ] + }, + "@tailwindcss/typography@0.5.19_tailwindcss@4.1.14": { + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dependencies": [ + "postcss-selector-parser", + "tailwindcss" + ] + }, + "@tanstack/react-virtual@3.13.12_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "dependencies": [ + "@tanstack/virtual-core", + "react", + "react-dom" + ] + }, + "@tanstack/virtual-core@3.13.12": { + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==" + }, + "@types/d3-array@3.0.3": { + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==" + }, + "@types/d3-color@3.1.0": { + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==" + }, + "@types/d3-color@3.1.3": { + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "@types/d3-delaunay@6.0.1": { + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==" + }, + "@types/d3-delaunay@6.0.4": { + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "@types/d3-ease@3.0.2": { + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "@types/d3-format@1.4.5": { + "integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==" + }, + "@types/d3-format@3.0.1": { + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" + }, + "@types/d3-geo@3.1.0": { + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": [ + "@types/geojson" + ] + }, + "@types/d3-interpolate@3.0.1": { + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dependencies": [ + "@types/d3-color@3.1.3" + ] + }, + "@types/d3-interpolate@3.0.4": { + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": [ + "@types/d3-color@3.1.3" + ] + }, + "@types/d3-path@1.0.11": { + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" + }, + "@types/d3-scale-chromatic@3.1.0": { + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" + }, + "@types/d3-scale@4.0.2": { + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "dependencies": [ + "@types/d3-time@3.0.4" + ] + }, + "@types/d3-scale@4.0.9": { + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": [ + "@types/d3-time@3.0.4" + ] + }, + "@types/d3-shape@1.3.12": { + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "dependencies": [ + "@types/d3-path" + ] + }, + "@types/d3-shape@3.1.7": { + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dependencies": [ + "@types/d3-path" + ] + }, + "@types/d3-time-format@2.1.0": { + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==" + }, + "@types/d3-time-format@2.3.4": { + "integrity": "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==" + }, + "@types/d3-time-format@3.0.4": { + "integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==" + }, + "@types/d3-time@1.1.4": { + "integrity": "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==" + }, + "@types/d3-time@3.0.0": { + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + }, + "@types/d3-time@3.0.4": { + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "@types/d3-timer@3.0.2": { + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "@types/d3-voronoi@1.1.12": { + "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==" + }, + "@types/debug@4.1.12": { + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": [ + "@types/ms" + ] + }, + "@types/estree-jsx@1.0.5": { + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": [ + "@types/estree" + ] + }, + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "@types/geojson@7946.0.16": { + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" + }, + "@types/hast@2.3.10": { + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": [ + "@types/unist@2.0.11" + ] + }, + "@types/hast@3.0.4": { + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": [ + "@types/unist@3.0.3" + ] + }, + "@types/lodash@4.17.20": { + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + }, + "@types/mdast@4.0.4": { + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": [ + "@types/unist@3.0.3" + ] + }, + "@types/ms@2.1.0": { + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "@types/parse-json@4.0.2": { + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "@types/prismjs@1.26.5": { + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==" + }, + "@types/prop-types@15.7.15": { + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" + }, + "@types/react-dom@19.2.2_@types+react@19.2.2": { + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dependencies": [ + "@types/react" + ] + }, + "@types/react-transition-group@4.4.12_@types+react@19.2.2": { + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dependencies": [ + "@types/react" + ] + }, + "@types/react@19.2.2": { + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dependencies": [ + "csstype" + ] + }, + "@types/unist@2.0.11": { + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "@types/unist@3.0.3": { + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "@uiw/copy-to-clipboard@1.0.17": { + "integrity": "sha512-O2GUHV90Iw2VrSLVLK0OmNIMdZ5fgEg4NhvtwINsX+eZ/Wf6DWD0TdsK9xwV7dNRnK/UI2mQtl0a2/kRgm1m1A==" + }, + "@uiw/react-markdown-preview@5.1.5_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-DNOqx1a6gJR7Btt57zpGEKTfHRlb7rWbtctMRO2f82wWcuoJsxPBrM+JWebDdOD0LfD8oe2CQvW2ICQJKHQhZg==", + "dependencies": [ + "@babel/runtime", + "@uiw/copy-to-clipboard", + "react", + "react-dom", + "react-markdown@9.0.3_@types+react@19.2.2_react@19.2.0", + "rehype-attr", + "rehype-autolink-headings", + "rehype-ignore", + "rehype-prism-plus", + "rehype-raw", + "rehype-rewrite", + "rehype-slug", + "remark-gfm", + "remark-github-blockquote-alert", + "unist-util-visit" + ] + }, + "@uiw/react-md-editor@4.0.8_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-S3mOzZeGmJNhzdXJxRTCwsFMDp8nBWeQUf59cK3L6QHzDUHnRoHpcmWpfVRyKGKSg8zaI2+meU5cYWf8kYn3mQ==", + "dependencies": [ + "@babel/runtime", + "@uiw/react-markdown-preview", + "react", + "react-dom", + "rehype", + "rehype-prism-plus" + ] + }, + "@ungap/structured-clone@1.3.0": { + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, + "@visx/annotation@3.12.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-ZH6Y4jfrb47iEUV9O2itU9TATE5IPzhs5qvP6J7vmv26qkqwDcuE7xN3S3l9R70WjyEKGbpO8js4EijA3FJWkA==", + "dependencies": [ + "@types/react", + "@visx/drag", + "@visx/group", + "@visx/text", + "classnames", + "prop-types", + "react", + "react-use-measure" + ] + }, + "@visx/axis@3.12.0_react@19.2.0": { + "integrity": "sha512-8MoWpfuaJkhm2Yg+HwyytK8nk+zDugCqTT/tRmQX7gW4LYrHYLXFUXOzbDyyBakCVaUbUaAhVFxpMASJiQKf7A==", + "dependencies": [ + "@types/react", + "@visx/group", + "@visx/point", + "@visx/scale", + "@visx/shape", + "@visx/text", + "classnames", + "prop-types", + "react" + ] + }, + "@visx/bounds@3.12.0_react@19.2.0_react-dom@19.2.0__react@19.2.0_@types+react@19.2.2": { + "integrity": "sha512-peAlNCUbYaaZ0IO6c1lDdEAnZv2iGPDiLIM8a6gu7CaMhtXZJkqrTh+AjidNcIqITktrICpGxJE/Qo9D099dvQ==", + "dependencies": [ + "@types/react", + "@types/react-dom", + "prop-types", + "react", + "react-dom" + ] + }, + "@visx/curve@3.12.0": { + "integrity": "sha512-Ng1mefXIzoIoAivw7dJ+ZZYYUbfuwXgZCgQynShr6ZIVw7P4q4HeQfJP3W24ON+1uCSrzoycHSXRelhR9SBPcw==", + "dependencies": [ + "@types/d3-shape@1.3.12", + "d3-shape@1.3.7" + ] + }, + "@visx/drag@3.12.0_react@19.2.0": { + "integrity": "sha512-LXOoPVw//YPjpYhDJYBsCYDuv1QimsXjDV98duH0aCy4V94ediXMQpe2wHq4pnlDobLEB71FjOZMFrbFmqtERg==", + "dependencies": [ + "@types/react", + "@visx/event", + "@visx/point", + "prop-types", + "react" + ] + }, + "@visx/event@3.12.0": { + "integrity": "sha512-9Lvw6qJ0Fi+y1vsC1WspfdIKCxHTb7oy59Uql1uBdPGT8zChP0vuxW0jQNQRDbKgoefj4pCXAFi8+MF1mEtVTw==", + "dependencies": [ + "@types/react", + "@visx/point" + ] + }, + "@visx/glyph@3.12.0_react@19.2.0": { + "integrity": "sha512-E9ST9MoPNyXQzjZxYYAGXT4CbBpnB90Qhx8UvUUM2/n/SZUNeH+m6UiB/CzT0jGK2b0lPHF91mlOiQ8JXBRhYg==", + "dependencies": [ + "@types/d3-shape@1.3.12", + "@types/react", + "@visx/group", + "classnames", + "d3-shape@1.3.7", + "prop-types", + "react" + ] + }, + "@visx/grid@3.12.0_react@19.2.0": { + "integrity": "sha512-L4ex2ooSYhwNIxJ3XFIKRhoSvEGjPc2Y3YCrtNB4TV5Ofdj4q0UMOsxfrH23Pr8HSHuQhb6VGMgYoK0LuWqDmQ==", + "dependencies": [ + "@types/react", + "@visx/curve", + "@visx/group", + "@visx/point", + "@visx/scale", + "@visx/shape", + "classnames", + "prop-types", + "react" + ] + }, + "@visx/group@3.12.0_react@19.2.0": { + "integrity": "sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==", + "dependencies": [ + "@types/react", + "classnames", + "prop-types", + "react" + ] + }, + "@visx/point@3.12.0": { + "integrity": "sha512-I6UrHoJAEVbx3RORQNupgTiX5EzjuZpiwLPxn8L2mI5nfERotPKi1Yus12Cq2WtXqEBR/WgqTnoImlqOXBykcA==" + }, + "@visx/react-spring@3.12.0_@react-spring+web@9.7.5__react@19.2.0__react-dom@19.2.0___react@19.2.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-ehtmjFrUQx3g0mZ684M4LgI9UfQ84ZWD/m9tKfvXhEm1Vl8D4AjaZ4af1tTOg9S7vk2VlpxvVOVN7+t5pu0nSA==", + "dependencies": [ + "@react-spring/web", + "@types/react", + "@visx/axis", + "@visx/grid", + "@visx/scale", + "@visx/text", + "classnames", + "prop-types", + "react" + ] + }, + "@visx/responsive@3.12.0_react@19.2.0": { + "integrity": "sha512-GV1BTYwAGlk/K5c9vH8lS2syPnTuIqEacI7L6LRPbsuaLscXMNi+i9fZyzo0BWvAdtRV8v6Urzglo++lvAXT1Q==", + "dependencies": [ + "@types/lodash", + "@types/react", + "lodash", + "prop-types", + "react" + ] + }, + "@visx/scale@3.12.0": { + "integrity": "sha512-+ubijrZ2AwWCsNey0HGLJ0YKNeC/XImEFsr9rM+Uef1CM3PNM43NDdNTrdBejSlzRq0lcfQPWYMYQFSlkLcPOg==", + "dependencies": [ + "@visx/vendor" + ] + }, + "@visx/shape@3.12.0_react@19.2.0": { + "integrity": "sha512-/1l0lrpX9tPic6SJEalryBKWjP/ilDRnQA+BGJTI1tj7i23mJ/J0t4nJHyA1GrL4QA/bM/qTJ35eyz5dEhJc4g==", + "dependencies": [ + "@types/d3-path", + "@types/d3-shape@1.3.12", + "@types/lodash", + "@types/react", + "@visx/curve", + "@visx/group", + "@visx/scale", + "classnames", + "d3-path@1.0.9", + "d3-shape@1.3.7", + "lodash", + "prop-types", + "react" + ] + }, + "@visx/text@3.12.0_react@19.2.0": { + "integrity": "sha512-0rbDYQlbuKPhBqXkkGYKFec1gQo05YxV45DORzr6hCyaizdJk1G+n9VkuKSHKBy1vVQhBA0W3u/WXd7tiODQPA==", + "dependencies": [ + "@types/lodash", + "@types/react", + "classnames", + "lodash", + "prop-types", + "react", + "reduce-css-calc" + ] + }, + "@visx/tooltip@3.12.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-pWhsYhgl0Shbeqf80qy4QCB6zpq6tQtMQQxKlh3UiKxzkkfl+Metaf9p0/S0HexNi4vewOPOo89xWx93hBeh3A==", + "dependencies": [ + "@types/react", + "@visx/bounds", + "classnames", + "prop-types", + "react", + "react-dom", + "react-use-measure" + ] + }, + "@visx/vendor@3.12.0": { + "integrity": "sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==", + "dependencies": [ + "@types/d3-array", + "@types/d3-color@3.1.0", + "@types/d3-delaunay@6.0.1", + "@types/d3-format@3.0.1", + "@types/d3-geo", + "@types/d3-interpolate@3.0.1", + "@types/d3-scale@4.0.2", + "@types/d3-time-format@2.1.0", + "@types/d3-time@3.0.0", + "d3-array@3.2.1", + "d3-color", + "d3-delaunay@6.0.2", + "d3-format@3.1.0", + "d3-geo@3.1.0", + "d3-interpolate", + "d3-scale", + "d3-time-format@4.1.0", + "d3-time@3.1.0", + "internmap" + ] + }, + "@visx/voronoi@3.12.0_react@19.2.0": { + "integrity": "sha512-U3HWu6g5UjQchFDq8k/A4U9WrlN+80rAFPdGOUvIGOueQw9RmlZlNaeg8IJcQr2yk1s4O/VSpt3nR82zdINWMw==", + "dependencies": [ + "@types/d3-voronoi", + "@types/react", + "classnames", + "d3-voronoi", + "prop-types", + "react" + ] + }, + "@visx/xychart@3.12.0_@react-spring+web@9.7.5__react@19.2.0__react-dom@19.2.0___react@19.2.0_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-itJ7qvj/STpVmHesVyo2vPOataBM1mgSaf9R6/s4Bpe340wZldfCJ+IqRcNgdtbBagz1Hlr/sRnla4tWE2yw9A==", + "dependencies": [ + "@react-spring/web", + "@types/lodash", + "@types/react", + "@visx/annotation", + "@visx/axis", + "@visx/event", + "@visx/glyph", + "@visx/grid", + "@visx/react-spring", + "@visx/responsive", + "@visx/scale", + "@visx/shape", + "@visx/text", + "@visx/tooltip", + "@visx/vendor", + "@visx/voronoi", + "classnames", + "d3-interpolate-path", + "d3-shape@2.1.0", + "lodash", + "mitt", + "prop-types", + "react" + ] + }, + "babel-plugin-macros@3.1.0": { + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": [ + "@babel/runtime", + "cosmiconfig", + "resolve" + ] + }, + "bail@2.0.2": { + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==" + }, + "balanced-match@0.4.2": { + "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==" + }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bcp-47-match@2.0.3": { + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==" + }, + "boolbase@1.0.0": { + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "buffer-from@0.1.2": { + "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camelcase-css@2.0.1": { + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + }, + "ccount@2.0.1": { + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" + }, + "character-entities-html4@2.1.0": { + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==" + }, + "character-entities-legacy@1.1.4": { + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==" + }, + "character-entities-legacy@3.0.0": { + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" + }, + "character-entities@1.2.4": { + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==" + }, + "character-entities@2.0.2": { + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==" + }, + "character-reference-invalid@1.1.4": { + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" + }, + "character-reference-invalid@2.0.1": { + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==" + }, + "classnames@2.5.1": { + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "clsx@2.1.1": { + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "comma-separated-tokens@1.0.8": { + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==" + }, + "comma-separated-tokens@2.0.3": { + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" + }, + "commander@7.2.0": { + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "convert-source-map@1.9.0": { + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "core-util-is@1.0.3": { + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cosmiconfig@7.1.0": { + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": [ + "@types/parse-json", + "import-fresh", + "parse-json", + "path-type", + "yaml" + ] + }, + "css-selector-parser@3.1.3": { + "integrity": "sha512-gJMigczVZqYAk0hPVzx/M4Hm1D9QOtqkdQk9005TNzDIUGzo5cnHEDiKUT7jGPximL/oYb+LIitcHFQ4aKupxg==" + }, + "css-selector-tokenizer@0.8.0": { + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dependencies": [ + "cssesc", + "fastparse" + ] + }, + "cssesc@3.0.0": { + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "csstype@3.1.3": { + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "culori@3.3.0": { + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==" + }, + "d3-array@3.2.1": { + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "dependencies": [ + "internmap" + ] + }, + "d3-array@3.2.4": { + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": [ + "internmap" + ] + }, + "d3-axis@3.0.0": { + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush@3.0.0_d3-selection@3.0.0": { + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": [ + "d3-dispatch", + "d3-drag", + "d3-interpolate", + "d3-selection", + "d3-transition" + ] + }, + "d3-chord@3.0.1": { + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": [ + "d3-path@3.1.0" + ] + }, + "d3-color@3.1.0": { + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour@4.0.2": { + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": [ + "d3-array@3.2.4" + ] + }, + "d3-delaunay@6.0.2": { + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dependencies": [ + "delaunator" + ] + }, + "d3-delaunay@6.0.4": { + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": [ + "delaunator" + ] + }, + "d3-dispatch@3.0.1": { + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag@3.0.0": { + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": [ + "d3-dispatch", + "d3-selection" + ] + }, + "d3-dsv@3.0.1": { + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": [ + "commander", + "iconv-lite", + "rw" + ] + }, + "d3-ease@3.0.1": { + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch@3.0.1": { + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": [ + "d3-dsv" + ] + }, + "d3-force@3.0.0": { + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": [ + "d3-dispatch", + "d3-quadtree", + "d3-timer" + ] + }, + "d3-format@1.4.5": { + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "d3-format@3.1.0": { + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo@3.1.0": { + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": [ + "d3-array@3.2.4" + ] + }, + "d3-geo@3.1.1": { + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": [ + "d3-array@3.2.4" + ] + }, + "d3-hierarchy@3.1.2": { + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate-path@2.2.1": { + "integrity": "sha512-6qLLh/KJVzls0XtMsMpcxhqMhgVEN7VIbR/6YGZe2qlS8KDgyyVB20XcmGnDyB051HcefQXM/Tppa9vcANEA4Q==" + }, + "d3-interpolate@3.0.1": { + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": [ + "d3-color" + ] + }, + "d3-path@1.0.9": { + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-path@3.1.0": { + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-polygon@3.0.1": { + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree@3.0.1": { + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random@3.0.1": { + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale-chromatic@3.1.0": { + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": [ + "d3-color", + "d3-interpolate" + ] + }, + "d3-scale@4.0.2": { + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": [ + "d3-array@3.2.4", + "d3-format@3.1.0", + "d3-interpolate", + "d3-time-format@4.1.0", + "d3-time@3.1.0" + ] + }, + "d3-selection@3.0.0": { + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape@1.3.7": { + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": [ + "d3-path@1.0.9" + ] + }, + "d3-shape@2.1.0": { + "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", + "dependencies": [ + "d3-path@1.0.9" + ] + }, + "d3-shape@3.2.0": { + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": [ + "d3-path@3.1.0" + ] + }, + "d3-time-format@3.0.0": { + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "dependencies": [ + "d3-time@1.1.0" + ] + }, + "d3-time-format@4.1.0": { + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": [ + "d3-time@3.1.0" + ] + }, + "d3-time@1.1.0": { + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time@3.1.0": { + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": [ + "d3-array@3.2.4" + ] + }, + "d3-timer@3.0.1": { + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition@3.0.1_d3-selection@3.0.0": { + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": [ + "d3-color", + "d3-dispatch", + "d3-ease", + "d3-interpolate", + "d3-selection", + "d3-timer" + ] + }, + "d3-voronoi@1.1.4": { + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom@3.0.0_d3-selection@3.0.0": { + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": [ + "d3-dispatch", + "d3-drag", + "d3-interpolate", + "d3-selection", + "d3-transition" + ] + }, + "d3@7.9.0_d3-selection@3.0.0": { + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": [ + "d3-array@3.2.4", + "d3-axis", + "d3-brush", + "d3-chord", + "d3-color", + "d3-contour", + "d3-delaunay@6.0.4", + "d3-dispatch", + "d3-drag", + "d3-dsv", + "d3-ease", + "d3-fetch", + "d3-force", + "d3-format@3.1.0", + "d3-geo@3.1.1", + "d3-hierarchy", + "d3-interpolate", + "d3-path@3.1.0", + "d3-polygon", + "d3-quadtree", + "d3-random", + "d3-scale", + "d3-scale-chromatic", + "d3-selection", + "d3-shape@3.2.0", + "d3-time-format@4.1.0", + "d3-time@3.1.0", + "d3-timer", + "d3-transition", + "d3-zoom" + ] + }, + "daisyui@4.12.24": { + "integrity": "sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==", + "dependencies": [ + "css-selector-tokenizer", + "culori", + "picocolors", + "postcss-js" + ] + }, + "date-fns@4.1.0": { + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "decimal.js-light@2.5.1": { + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, + "decode-named-character-reference@1.2.0": { + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "dependencies": [ + "character-entities@2.0.2" + ] + }, + "delaunator@5.0.1": { + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": [ + "robust-predicates" + ] + }, + "dequal@2.0.3": { + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" + }, + "devlop@1.1.0": { + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": [ + "dequal" + ] + }, + "direction@2.0.1": { + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==" + }, + "dom-helpers@5.2.1": { + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": [ + "@babel/runtime", + "csstype" + ] + }, + "duplexer2@0.1.4": { + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": [ + "readable-stream@2.3.8" + ] + }, + "entities@6.0.1": { + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==" + }, + "error-ex@1.3.4": { + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dependencies": [ + "is-arrayish" + ] + }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "escape-string-regexp@5.0.0": { + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==" + }, + "estree-util-is-identifier-name@3.0.0": { + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==" + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "extend@3.0.2": { + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-equals@5.3.2": { + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==" + }, + "fastparse@1.1.2": { + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" + }, + "fault@1.0.4": { + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": [ + "format" + ] + }, + "find-root@1.1.0": { + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "format@0.2.2": { + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==" + }, + "function-bind@1.1.2": { + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "github-slugger@2.0.0": { + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "hasown@2.0.2": { + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": [ + "function-bind" + ] + }, + "hast-util-from-html@2.0.3": { + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "dependencies": [ + "@types/hast@3.0.4", + "devlop", + "hast-util-from-parse5", + "parse5", + "vfile", + "vfile-message" + ] + }, + "hast-util-from-parse5@8.0.3": { + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/unist@3.0.3", + "devlop", + "hastscript@9.0.1", + "property-information@7.1.0", + "vfile", + "vfile-location", + "web-namespaces" + ] + }, + "hast-util-has-property@3.0.0": { + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "dependencies": [ + "@types/hast@3.0.4" + ] + }, + "hast-util-heading-rank@3.0.0": { + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dependencies": [ + "@types/hast@3.0.4" + ] + }, + "hast-util-is-element@3.0.0": { + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": [ + "@types/hast@3.0.4" + ] + }, + "hast-util-parse-selector@2.2.5": { + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==" + }, + "hast-util-parse-selector@3.1.1": { + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": [ + "@types/hast@2.3.10" + ] + }, + "hast-util-parse-selector@4.0.0": { + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": [ + "@types/hast@3.0.4" + ] + }, + "hast-util-raw@9.1.0": { + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/unist@3.0.3", + "@ungap/structured-clone", + "hast-util-from-parse5", + "hast-util-to-parse5", + "html-void-elements", + "mdast-util-to-hast", + "parse5", + "unist-util-position", + "unist-util-visit", + "vfile", + "web-namespaces", + "zwitch" + ] + }, + "hast-util-select@6.0.4": { + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/unist@3.0.3", + "bcp-47-match", + "comma-separated-tokens@2.0.3", + "css-selector-parser", + "devlop", + "direction", + "hast-util-has-property", + "hast-util-to-string", + "hast-util-whitespace", + "nth-check", + "property-information@7.1.0", + "space-separated-tokens@2.0.2", + "unist-util-visit", + "zwitch" + ] + }, + "hast-util-to-html@9.0.5": { + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/unist@3.0.3", + "ccount", + "comma-separated-tokens@2.0.3", + "hast-util-whitespace", + "html-void-elements", + "mdast-util-to-hast", + "property-information@7.1.0", + "space-separated-tokens@2.0.2", + "stringify-entities", + "zwitch" + ] + }, + "hast-util-to-jsx-runtime@2.3.6": { + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dependencies": [ + "@types/estree", + "@types/hast@3.0.4", + "@types/unist@3.0.3", + "comma-separated-tokens@2.0.3", + "devlop", + "estree-util-is-identifier-name", + "hast-util-whitespace", + "mdast-util-mdx-expression", + "mdast-util-mdx-jsx", + "mdast-util-mdxjs-esm", + "property-information@7.1.0", + "space-separated-tokens@2.0.2", + "style-to-js", + "unist-util-position", + "vfile-message" + ] + }, + "hast-util-to-parse5@8.0.0": { + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dependencies": [ + "@types/hast@3.0.4", + "comma-separated-tokens@2.0.3", + "devlop", + "property-information@6.5.0", + "space-separated-tokens@2.0.2", + "web-namespaces", + "zwitch" + ] + }, + "hast-util-to-string@3.0.1": { + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "dependencies": [ + "@types/hast@3.0.4" + ] + }, + "hast-util-whitespace@3.0.0": { + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": [ + "@types/hast@3.0.4" + ] + }, + "hastscript@6.0.0": { + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": [ + "@types/hast@2.3.10", + "comma-separated-tokens@1.0.8", + "hast-util-parse-selector@2.2.5", + "property-information@5.6.0", + "space-separated-tokens@1.1.5" + ] + }, + "hastscript@7.2.0": { + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": [ + "@types/hast@2.3.10", + "comma-separated-tokens@2.0.3", + "hast-util-parse-selector@3.1.1", + "property-information@6.5.0", + "space-separated-tokens@2.0.2" + ] + }, + "hastscript@9.0.1": { + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "dependencies": [ + "@types/hast@3.0.4", + "comma-separated-tokens@2.0.3", + "hast-util-parse-selector@4.0.0", + "property-information@7.1.0", + "space-separated-tokens@2.0.2" + ] + }, + "highlight.js@10.7.3": { + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" + }, + "highlightjs-vue@1.0.0": { + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" + }, + "hoist-non-react-statics@3.3.2": { + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": [ + "react-is@16.13.1" + ] + }, + "html-tokenize@2.0.1": { + "integrity": "sha512-QY6S+hZ0f5m1WT8WffYN+Hg+xm/w5I8XeUcAq/ZYP5wVC8xbKi4Whhru3FtrAebD5EhBW8rmFzkDI6eCAuFe2w==", + "dependencies": [ + "buffer-from", + "inherits", + "minimist", + "readable-stream@1.0.34", + "through2" + ] + }, + "html-url-attributes@3.0.1": { + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==" + }, + "html-void-elements@3.0.0": { + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==" + }, + "iconv-lite@0.6.3": { + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": [ + "safer-buffer" + ] + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inline-style-parser@0.2.4": { + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, + "internmap@2.0.3": { + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, + "is-alphabetical@1.0.4": { + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==" + }, + "is-alphabetical@2.0.1": { + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==" + }, + "is-alphanumerical@1.0.4": { + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": [ + "is-alphabetical@1.0.4", + "is-decimal@1.0.4" + ] + }, + "is-alphanumerical@2.0.1": { + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": [ + "is-alphabetical@2.0.1", + "is-decimal@2.0.1" + ] + }, + "is-arrayish@0.2.1": { + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-core-module@2.16.1": { + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": [ + "hasown" + ] + }, + "is-decimal@1.0.4": { + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" + }, + "is-decimal@2.0.1": { + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==" + }, + "is-hexadecimal@1.0.4": { + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" + }, + "is-hexadecimal@2.0.1": { + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==" + }, + "is-plain-obj@4.1.0": { + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==" + }, + "isarray@0.0.1": { + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "isarray@1.0.0": { + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsesc@3.1.0": { + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" + }, + "json-parse-even-better-errors@2.3.1": { + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "lines-and-columns@1.2.4": { + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "lodash@4.17.21": { + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "longest-streak@3.1.0": { + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==" + }, + "loose-envify@1.4.0": { + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": [ + "js-tokens" + ] + }, + "lowlight@1.20.0": { + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dependencies": [ + "fault", + "highlight.js" + ] + }, + "markdown-table@3.0.4": { + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==" + }, + "math-expression-evaluator@1.4.0": { + "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==" + }, + "mdast-util-find-and-replace@3.0.2": { + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dependencies": [ + "@types/mdast", + "escape-string-regexp@5.0.0", + "unist-util-is", + "unist-util-visit-parents" + ] + }, + "mdast-util-from-markdown@2.0.2": { + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": [ + "@types/mdast", + "@types/unist@3.0.3", + "decode-named-character-reference", + "devlop", + "mdast-util-to-string", + "micromark", + "micromark-util-decode-numeric-character-reference", + "micromark-util-decode-string", + "micromark-util-normalize-identifier", + "micromark-util-symbol", + "micromark-util-types", + "unist-util-stringify-position" + ] + }, + "mdast-util-gfm-autolink-literal@2.0.1": { + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dependencies": [ + "@types/mdast", + "ccount", + "devlop", + "mdast-util-find-and-replace", + "micromark-util-character" + ] + }, + "mdast-util-gfm-footnote@2.1.0": { + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dependencies": [ + "@types/mdast", + "devlop", + "mdast-util-from-markdown", + "mdast-util-to-markdown", + "micromark-util-normalize-identifier" + ] + }, + "mdast-util-gfm-strikethrough@2.0.0": { + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": [ + "@types/mdast", + "mdast-util-from-markdown", + "mdast-util-to-markdown" + ] + }, + "mdast-util-gfm-table@2.0.0": { + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": [ + "@types/mdast", + "devlop", + "markdown-table", + "mdast-util-from-markdown", + "mdast-util-to-markdown" + ] + }, + "mdast-util-gfm-task-list-item@2.0.0": { + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": [ + "@types/mdast", + "devlop", + "mdast-util-from-markdown", + "mdast-util-to-markdown" + ] + }, + "mdast-util-gfm@3.1.0": { + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dependencies": [ + "mdast-util-from-markdown", + "mdast-util-gfm-autolink-literal", + "mdast-util-gfm-footnote", + "mdast-util-gfm-strikethrough", + "mdast-util-gfm-table", + "mdast-util-gfm-task-list-item", + "mdast-util-to-markdown" + ] + }, + "mdast-util-mdx-expression@2.0.1": { + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": [ + "@types/estree-jsx", + "@types/hast@3.0.4", + "@types/mdast", + "devlop", + "mdast-util-from-markdown", + "mdast-util-to-markdown" + ] + }, + "mdast-util-mdx-jsx@3.2.0": { + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": [ + "@types/estree-jsx", + "@types/hast@3.0.4", + "@types/mdast", + "@types/unist@3.0.3", + "ccount", + "devlop", + "mdast-util-from-markdown", + "mdast-util-to-markdown", + "parse-entities@4.0.2", + "stringify-entities", + "unist-util-stringify-position", + "vfile-message" + ] + }, + "mdast-util-mdxjs-esm@2.0.1": { + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": [ + "@types/estree-jsx", + "@types/hast@3.0.4", + "@types/mdast", + "devlop", + "mdast-util-from-markdown", + "mdast-util-to-markdown" + ] + }, + "mdast-util-phrasing@4.1.0": { + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": [ + "@types/mdast", + "unist-util-is" + ] + }, + "mdast-util-to-hast@13.2.0": { + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/mdast", + "@ungap/structured-clone", + "devlop", + "micromark-util-sanitize-uri", + "trim-lines", + "unist-util-position", + "unist-util-visit", + "vfile" + ] + }, + "mdast-util-to-markdown@2.1.2": { + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dependencies": [ + "@types/mdast", + "@types/unist@3.0.3", + "longest-streak", + "mdast-util-phrasing", + "mdast-util-to-string", + "micromark-util-classify-character", + "micromark-util-decode-string", + "unist-util-visit", + "zwitch" + ] + }, + "mdast-util-to-string@4.0.0": { + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": [ + "@types/mdast" + ] + }, + "micromark-core-commonmark@2.0.3": { + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dependencies": [ + "decode-named-character-reference", + "devlop", + "micromark-factory-destination", + "micromark-factory-label", + "micromark-factory-space", + "micromark-factory-title", + "micromark-factory-whitespace", + "micromark-util-character", + "micromark-util-chunked", + "micromark-util-classify-character", + "micromark-util-html-tag-name", + "micromark-util-normalize-identifier", + "micromark-util-resolve-all", + "micromark-util-subtokenize", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-extension-gfm-autolink-literal@2.1.0": { + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": [ + "micromark-util-character", + "micromark-util-sanitize-uri", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-extension-gfm-footnote@2.1.0": { + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": [ + "devlop", + "micromark-core-commonmark", + "micromark-factory-space", + "micromark-util-character", + "micromark-util-normalize-identifier", + "micromark-util-sanitize-uri", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-extension-gfm-strikethrough@2.1.0": { + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": [ + "devlop", + "micromark-util-chunked", + "micromark-util-classify-character", + "micromark-util-resolve-all", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-extension-gfm-table@2.1.1": { + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dependencies": [ + "devlop", + "micromark-factory-space", + "micromark-util-character", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-extension-gfm-tagfilter@2.0.0": { + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": [ + "micromark-util-types" + ] + }, + "micromark-extension-gfm-task-list-item@2.1.0": { + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": [ + "devlop", + "micromark-factory-space", + "micromark-util-character", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-extension-gfm@3.0.0": { + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": [ + "micromark-extension-gfm-autolink-literal", + "micromark-extension-gfm-footnote", + "micromark-extension-gfm-strikethrough", + "micromark-extension-gfm-table", + "micromark-extension-gfm-tagfilter", + "micromark-extension-gfm-task-list-item", + "micromark-util-combine-extensions", + "micromark-util-types" + ] + }, + "micromark-factory-destination@2.0.1": { + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dependencies": [ + "micromark-util-character", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-factory-label@2.0.1": { + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dependencies": [ + "devlop", + "micromark-util-character", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-factory-space@2.0.1": { + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dependencies": [ + "micromark-util-character", + "micromark-util-types" + ] + }, + "micromark-factory-title@2.0.1": { + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dependencies": [ + "micromark-factory-space", + "micromark-util-character", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-factory-whitespace@2.0.1": { + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dependencies": [ + "micromark-factory-space", + "micromark-util-character", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-util-character@2.1.1": { + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dependencies": [ + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-util-chunked@2.0.1": { + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dependencies": [ + "micromark-util-symbol" + ] + }, + "micromark-util-classify-character@2.0.1": { + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dependencies": [ + "micromark-util-character", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-util-combine-extensions@2.0.1": { + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dependencies": [ + "micromark-util-chunked", + "micromark-util-types" + ] + }, + "micromark-util-decode-numeric-character-reference@2.0.2": { + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dependencies": [ + "micromark-util-symbol" + ] + }, + "micromark-util-decode-string@2.0.1": { + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dependencies": [ + "decode-named-character-reference", + "micromark-util-character", + "micromark-util-decode-numeric-character-reference", + "micromark-util-symbol" + ] + }, + "micromark-util-encode@2.0.1": { + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" + }, + "micromark-util-html-tag-name@2.0.1": { + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==" + }, + "micromark-util-normalize-identifier@2.0.1": { + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dependencies": [ + "micromark-util-symbol" + ] + }, + "micromark-util-resolve-all@2.0.1": { + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dependencies": [ + "micromark-util-types" + ] + }, + "micromark-util-sanitize-uri@2.0.1": { + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dependencies": [ + "micromark-util-character", + "micromark-util-encode", + "micromark-util-symbol" + ] + }, + "micromark-util-subtokenize@2.1.0": { + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dependencies": [ + "devlop", + "micromark-util-chunked", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "micromark-util-symbol@2.0.1": { + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" + }, + "micromark-util-types@2.0.2": { + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" + }, + "micromark@4.0.2": { + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dependencies": [ + "@types/debug", + "debug", + "decode-named-character-reference", + "devlop", + "micromark-core-commonmark", + "micromark-factory-space", + "micromark-util-character", + "micromark-util-chunked", + "micromark-util-combine-extensions", + "micromark-util-decode-numeric-character-reference", + "micromark-util-encode", + "micromark-util-normalize-identifier", + "micromark-util-resolve-all", + "micromark-util-sanitize-uri", + "micromark-util-subtokenize", + "micromark-util-symbol", + "micromark-util-types" + ] + }, + "minimist@1.2.8": { + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mitt@2.1.0": { + "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "multipipe@1.0.2": { + "integrity": "sha512-6uiC9OvY71vzSGX8lZvSqscE7ft9nPupJ8fMjrCNRAUy2LREUW42UL+V/NTrogr6rFgRydUrCX4ZitfpSNkSCQ==", + "dependencies": [ + "duplexer2", + "object-assign" + ] + }, + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" + }, + "nth-check@2.1.1": { + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": [ + "boolbase" + ] + }, + "object-assign@4.1.1": { + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-keys@0.4.0": { + "integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==" + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "parse-entities@2.0.0": { + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": [ + "character-entities-legacy@1.1.4", + "character-entities@1.2.4", + "character-reference-invalid@1.1.4", + "is-alphanumerical@1.0.4", + "is-decimal@1.0.4", + "is-hexadecimal@1.0.4" + ] + }, + "parse-entities@4.0.2": { + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dependencies": [ + "@types/unist@2.0.11", + "character-entities-legacy@3.0.0", + "character-reference-invalid@2.0.1", + "decode-named-character-reference", + "is-alphanumerical@2.0.1", + "is-decimal@2.0.1", + "is-hexadecimal@2.0.1" + ] + }, + "parse-json@5.2.0": { + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": [ + "@babel/code-frame", + "error-ex", + "json-parse-even-better-errors", + "lines-and-columns" + ] + }, + "parse-numeric-range@1.3.0": { + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, + "parse5@7.3.0": { + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dependencies": [ + "entities" + ] + }, + "path-parse@1.0.7": { + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-type@4.0.0": { + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "postcss-js@4.1.0_postcss@8.5.6": { + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dependencies": [ + "camelcase-css", + "postcss" + ] + }, + "postcss-selector-parser@6.0.10": { + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss@8.5.6": { + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "prismjs@1.27.0": { + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==" + }, + "prismjs@1.30.0": { + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==" + }, + "process-nextick-args@2.0.1": { + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "prop-types@15.8.1": { + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": [ + "loose-envify", + "object-assign", + "react-is@16.13.1" + ] + }, + "property-information@5.6.0": { + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dependencies": [ + "xtend@4.0.2" + ] + }, + "property-information@6.5.0": { + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==" + }, + "property-information@7.1.0": { + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" + }, + "react-dom@19.2.0_react@19.2.0": { + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "dependencies": [ + "react", + "scheduler" + ] + }, + "react-is@16.13.1": { + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-is@18.3.1": { + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "react-is@19.2.0": { + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==" + }, + "react-markdown@9.0.3_@types+react@19.2.2_react@19.2.0": { + "integrity": "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/react", + "devlop", + "hast-util-to-jsx-runtime", + "html-url-attributes", + "mdast-util-to-hast", + "react", + "remark-parse", + "remark-rehype", + "unified", + "unist-util-visit", + "vfile" + ] + }, + "react-markdown@9.1.0_@types+react@19.2.2_react@19.2.0": { + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/mdast", + "@types/react", + "devlop", + "hast-util-to-jsx-runtime", + "html-url-attributes", + "mdast-util-to-hast", + "react", + "remark-parse", + "remark-rehype", + "unified", + "unist-util-visit", + "vfile" + ] + }, + "react-smooth@4.0.4_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "dependencies": [ + "fast-equals", + "prop-types", + "react", + "react-dom", + "react-transition-group" + ] + }, + "react-syntax-highlighter@15.6.6_react@19.2.0": { + "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "dependencies": [ + "@babel/runtime", + "highlight.js", + "highlightjs-vue", + "lowlight", + "prismjs@1.30.0", + "react", + "refractor@3.6.0" + ] + }, + "react-transition-group@4.4.5_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": [ + "@babel/runtime", + "dom-helpers", + "loose-envify", + "prop-types", + "react", + "react-dom" + ] + }, + "react-use-measure@2.1.7_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "dependencies": [ + "react", + "react-dom" + ] + }, + "react@19.2.0": { + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==" + }, + "readable-stream@1.0.34": { + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dependencies": [ + "core-util-is", + "inherits", + "isarray@0.0.1", + "string_decoder@0.10.31" + ] + }, + "readable-stream@2.3.8": { + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": [ + "core-util-is", + "inherits", + "isarray@1.0.0", + "process-nextick-args", + "safe-buffer", + "string_decoder@1.1.1", + "util-deprecate" + ] + }, + "recharts-scale@0.4.5": { + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": [ + "decimal.js-light" + ] + }, + "recharts@2.15.4_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "dependencies": [ + "clsx", + "eventemitter3", + "lodash", + "react", + "react-dom", + "react-is@18.3.1", + "react-smooth", + "recharts-scale", + "tiny-invariant", + "victory-vendor" + ] + }, + "reduce-css-calc@1.3.0": { + "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", + "dependencies": [ + "balanced-match@0.4.2", + "math-expression-evaluator", + "reduce-function-call" + ] + }, + "reduce-function-call@1.0.3": { + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "dependencies": [ + "balanced-match@1.0.2" + ] + }, + "refractor@3.6.0": { + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dependencies": [ + "hastscript@6.0.0", + "parse-entities@2.0.0", + "prismjs@1.27.0" + ] + }, + "refractor@4.9.0": { + "integrity": "sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==", + "dependencies": [ + "@types/hast@2.3.10", + "@types/prismjs", + "hastscript@7.2.0", + "parse-entities@4.0.2" + ] + }, + "rehype-attr@3.0.3": { + "integrity": "sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==", + "dependencies": [ + "unified", + "unist-util-visit" + ] + }, + "rehype-autolink-headings@7.1.0": { + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "dependencies": [ + "@types/hast@3.0.4", + "@ungap/structured-clone", + "hast-util-heading-rank", + "hast-util-is-element", + "unified", + "unist-util-visit" + ] + }, + "rehype-ignore@2.0.2": { + "integrity": "sha512-BpAT/3lU9DMJ2siYVD/dSR0A/zQgD6Fb+fxkJd4j+wDVy6TYbYpK+FZqu8eM9EuNKGvi4BJR7XTZ/+zF02Dq8w==", + "dependencies": [ + "hast-util-select", + "unified", + "unist-util-visit" + ] + }, + "rehype-parse@9.0.1": { + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "dependencies": [ + "@types/hast@3.0.4", + "hast-util-from-html", + "unified" + ] + }, + "rehype-prism-plus@2.0.0": { + "integrity": "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==", + "dependencies": [ + "hast-util-to-string", + "parse-numeric-range", + "refractor@4.9.0", + "rehype-parse", + "unist-util-filter", + "unist-util-visit" + ] + }, + "rehype-raw@7.0.0": { + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dependencies": [ + "@types/hast@3.0.4", + "hast-util-raw", + "vfile" + ] + }, + "rehype-rewrite@4.0.2": { + "integrity": "sha512-rjLJ3z6fIV11phwCqHp/KRo8xuUCO8o9bFJCNw5o6O2wlLk6g8r323aRswdGBQwfXPFYeSuZdAjp4tzo6RGqEg==", + "dependencies": [ + "hast-util-select", + "unified", + "unist-util-visit" + ] + }, + "rehype-slug@6.0.0": { + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dependencies": [ + "@types/hast@3.0.4", + "github-slugger", + "hast-util-heading-rank", + "hast-util-to-string", + "unist-util-visit" + ] + }, + "rehype-stringify@10.0.1": { + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "dependencies": [ + "@types/hast@3.0.4", + "hast-util-to-html", + "unified" + ] + }, + "rehype@13.0.2": { + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "dependencies": [ + "@types/hast@3.0.4", + "rehype-parse", + "rehype-stringify", + "unified" + ] + }, + "remark-gfm@4.0.1": { + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dependencies": [ + "@types/mdast", + "mdast-util-gfm", + "micromark-extension-gfm", + "remark-parse", + "remark-stringify", + "unified" + ] + }, + "remark-github-blockquote-alert@1.3.1": { + "integrity": "sha512-OPNnimcKeozWN1w8KVQEuHOxgN3L4rah8geMOLhA5vN9wITqU4FWD+G26tkEsCGHiOVDbISx+Se5rGZ+D1p0Jg==", + "dependencies": [ + "unist-util-visit" + ] + }, + "remark-parse@11.0.0": { + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": [ + "@types/mdast", + "mdast-util-from-markdown", + "micromark-util-types", + "unified" + ] + }, + "remark-rehype@11.1.2": { + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dependencies": [ + "@types/hast@3.0.4", + "@types/mdast", + "mdast-util-to-hast", + "unified", + "vfile" + ] + }, + "remark-stringify@11.0.0": { + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": [ + "@types/mdast", + "mdast-util-to-markdown", + "unified" + ] + }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve@1.22.10": { + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": [ + "is-core-module", + "path-parse", + "supports-preserve-symlinks-flag" + ] + }, + "robust-predicates@3.0.2": { + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "rw@1.3.3": { + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "safe-buffer@5.1.2": { + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "scheduler@0.27.0": { + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "source-map@0.5.7": { + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + }, + "space-separated-tokens@1.1.5": { + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==" + }, + "space-separated-tokens@2.0.2": { + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==" + }, + "string_decoder@0.10.31": { + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "string_decoder@1.1.1": { + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": [ + "safe-buffer" + ] + }, + "stringify-entities@4.0.4": { + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": [ + "character-entities-html4", + "character-entities-legacy@3.0.0" + ] + }, + "style-to-js@1.1.18": { + "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==", + "dependencies": [ + "style-to-object" + ] + }, + "style-to-object@1.0.11": { + "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==", + "dependencies": [ + "inline-style-parser" + ] + }, + "stylis@4.2.0": { + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "supports-preserve-symlinks-flag@1.0.0": { + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "tabbable@6.2.0": { + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, + "tailwindcss@4.1.14": { + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==" + }, + "through2@0.4.2": { + "integrity": "sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", + "dependencies": [ + "readable-stream@1.0.34", + "xtend@2.1.2" + ] + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "tiny-invariant@1.3.3": { + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "trim-lines@3.0.1": { + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" + }, + "trough@2.2.0": { + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==" + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "unified@11.0.5": { + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": [ + "@types/unist@3.0.3", + "bail", + "devlop", + "extend", + "is-plain-obj", + "trough", + "vfile" + ] + }, + "unist-util-filter@5.0.1": { + "integrity": "sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==", + "dependencies": [ + "@types/unist@3.0.3", + "unist-util-is", + "unist-util-visit-parents" + ] + }, + "unist-util-is@6.0.0": { + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": [ + "@types/unist@3.0.3" + ] + }, + "unist-util-position@5.0.0": { + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": [ + "@types/unist@3.0.3" + ] + }, + "unist-util-stringify-position@4.0.0": { + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": [ + "@types/unist@3.0.3" + ] + }, + "unist-util-visit-parents@6.0.1": { + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": [ + "@types/unist@3.0.3", + "unist-util-is" + ] + }, + "unist-util-visit@5.0.0": { + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": [ + "@types/unist@3.0.3", + "unist-util-is", + "unist-util-visit-parents" + ] + }, + "use-sync-external-store@1.6.0_react@19.2.0": { + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dependencies": [ + "react" + ] + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "vfile-location@5.0.3": { + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "dependencies": [ + "@types/unist@3.0.3", + "vfile" + ] + }, + "vfile-message@4.0.3": { + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": [ + "@types/unist@3.0.3", + "unist-util-stringify-position" + ] + }, + "vfile@6.0.3": { + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": [ + "@types/unist@3.0.3", + "vfile-message" + ] + }, + "victory-vendor@36.9.2": { + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": [ + "@types/d3-array", + "@types/d3-ease", + "@types/d3-interpolate@3.0.4", + "@types/d3-scale@4.0.9", + "@types/d3-shape@3.1.7", + "@types/d3-time@3.0.4", + "@types/d3-timer", + "d3-array@3.2.4", + "d3-ease", + "d3-interpolate", + "d3-scale", + "d3-shape@3.2.0", + "d3-time@3.1.0", + "d3-timer" + ] + }, + "web-namespaces@2.0.1": { + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==" + }, + "xtend@2.1.2": { + "integrity": "sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", + "dependencies": [ + "object-keys" + ] + }, + "xtend@4.0.2": { + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yaml@1.10.2": { + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "zwitch@2.0.4": { + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" + } + }, + "remote": { + "https://deno.land/std@0.208.0/async/delay.ts": "a6142eb44cdd856b645086af2b811b1fcce08ec06bb7d50969e6a872ee9b8659", + "https://deno.land/std@0.208.0/http/server.ts": "f3cde6672e631d3e00785743cfa96bfed275618c0352c5ae84abbe5a2e0e4afc" + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@emotion/react@^11.14.0", + "npm:@emotion/server@^11.11.0", + "npm:@emotion/styled@^11.14.0", + "npm:@headlessui/react@^2.2.0", + "npm:@mui/material@^6.4.3", + "npm:@mui/x-charts@^7.26.0", + "npm:@nivo/line@0.88", + "npm:@tailwindcss/typography@~0.5.16", + "npm:@uiw/react-markdown-preview@^5.1.3", + "npm:@uiw/react-md-editor@^4.0.5", + "npm:@visx/xychart@^3.12.0", + "npm:d3@^7.9.0", + "npm:daisyui@^4.12.23", + "npm:date-fns@^4.1.0", + "npm:react-dom@19", + "npm:react-markdown@^9.0.3", + "npm:react-syntax-highlighter@^15.6.1", + "npm:react@19", + "npm:recharts@^2.15.1", + "npm:remark-gfm@4" + ] + } + } +} diff --git a/react_demo/dev.sh b/react_demo/dev.sh new file mode 100755 index 0000000..e078805 --- /dev/null +++ b/react_demo/dev.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# ReactDemo Development Server Launcher +# Usage: ./dev.sh [bun|deno] + +RUNTIME=${1:-bun} + +echo "🚀 Starting ReactDemo with $RUNTIME runtime..." +echo "📝 Runtime: $RUNTIME" +echo "🌐 Server will be available at: http://localhost:4666" +echo "" + +# Set the runtime environment variable +export REACT_RUNTIME=$RUNTIME + +# Start the Phoenix server +mix phx.server \ No newline at end of file diff --git a/server_deno.js b/server_deno.js new file mode 100644 index 0000000..a943a65 --- /dev/null +++ b/server_deno.js @@ -0,0 +1,158 @@ +import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; +import { renderToReadableStream, renderToString, renderToStaticMarkup } from "npm:react-dom/server"; + +const __comMap = {}; + +import { Component as __component_0 } from "/home/gao/Workspace/gsmlg-dev/phoenix-react/react_demo/assets/component/markdown.js"; +__comMap["markdown"] = __component_0; + +import { Component as __component_1 } from "/home/gao/Workspace/gsmlg-dev/phoenix-react/react_demo/assets/component/live_form.js"; +__comMap["live_form"] = __component_1; + +import { Component as __component_2 } from "/home/gao/Workspace/gsmlg-dev/phoenix-react/react_demo/assets/component/system_usage.js"; +__comMap["system_usage"] = __component_2; + + +const { COMPONENT_BASE, DENO_ENV } = Deno.env.toObject(); + +const isDev = DENO_ENV === 'development'; + +const port = parseInt(Deno.env.get("PORT") || "5226"); + +const handler = async (req) => { + try { + let bodyStream = req.body; + if (isDev) { + const bodyText = await req.text(); + console.log('Request: ', req.method, req.url, bodyText); + bodyStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(bodyText)); + controller.close(); + } + }); + } + const { url } = req; + const uri = new URL(url); + const { pathname } = uri; + + if (pathname.startsWith('/stop')) { + return new Response('{"message":"ok"}', { + headers: { + "Content-Type": "application/json", + }, + }); + } + + if (pathname.startsWith('/render_to_static_markup/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_static_markup\//, ''); + const Component = __comMap[fileName]; + if (!Component) { + return new Response(`Not Found, component not found.`, { + status: 404, + headers: { + "Content-Type": "text/html", + }, + }); + } + const jsxNode = React.createElement(Component, props); + const html = renderToStaticMarkup(jsxNode); + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + if (pathname.startsWith('/render_to_string/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_string\//, ''); + const Component = __comMap[fileName]; + const jsxNode = React.createElement(Component, props); + const html = renderToString(jsxNode); + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + if (pathname.startsWith('/render_to_readable_stream/')) { + const props = await req.json(); + const fileName = pathname.replace(/^\/render_to_readable_stream\//, ''); + const Component = __comMap[fileName]; + const jsxNode = React.createElement(Component, props); + const stream = await renderToReadableStream(jsxNode); + return new Response(stream, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + return new Response(`Not Found, not matched request.`, { + status: 404, + headers: { + "Content-Type": "text/html", + }, + }); + } catch(error) { + const html = ` + + `; + return new Response(html, { + status: 500, + headers: { + "Content-Type": "text/html", + }, + }); + } +}; + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +console.log(`Server started at http://localhost:${port}`); +console.log(`COMPONENT_BASE`, COMPONENT_BASE); +console.log(`DENO_ENV`, DENO_ENV); + +const ppid = Deno.pid; +const checkParentInterval = setInterval(() => { + try { + // Try to check if parent process still exists + Deno.kill(ppid, "0"); + } catch (e) { + console.log("Parent process exited. Shutting down server..."); + clearInterval(checkParentInterval); + Deno.exit(0); + } +}, 1000); + +const shutdown = async (signal) => { + console.log(`\nReceived ${signal}. Cleaning up...`); + clearInterval(checkParentInterval); + console.log("Cleanup done. Exiting."); + Deno.exit(0); +}; + +Deno.addSignalListener("SIGINT", () => { + shutdown("SIGINT"); +}); + +Deno.addSignalListener("SIGTERM", () => { + shutdown("SIGTERM"); +}); + +await serve(handler, { port }); \ No newline at end of file diff --git a/test/components/test_component.js b/test/components/test_component.js new file mode 100644 index 0000000..482dcdf --- /dev/null +++ b/test/components/test_component.js @@ -0,0 +1,3 @@ +export const Component = (props = {}) => { + return React.createElement('div', { className: 'test-component' }, `Hello ${props.name || 'World'}!`); +}; diff --git a/test/components_deno/test_component.jsx b/test/components_deno/test_component.jsx new file mode 100644 index 0000000..c891e81 --- /dev/null +++ b/test/components_deno/test_component.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Component = (props = {}) => { + return
Hello {props.name || 'World'}!
; +}; diff --git a/test/fixtures/markdown.js b/test/fixtures/markdown.js index fe3c540..25fe8bd 100644 --- a/test/fixtures/markdown.js +++ b/test/fixtures/markdown.js @@ -1,25 +1,8 @@ import * as React from 'react'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -import MarkdownPreview from '@uiw/react-markdown-preview'; - -import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'; -import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism'; export const Component = (props = {}) => { - - // return ( - // - // ); return ( { components={{ code(props) { const {children, className, node, ...rest} = props - const match = /language-(\w+)/.exec(className || '') - return match ? ( - - ) : ( - + return ( + {children} ) diff --git a/test/phoenix/mix/build/deno_integration_test.exs b/test/phoenix/mix/build/deno_integration_test.exs new file mode 100644 index 0000000..604c60c --- /dev/null +++ b/test/phoenix/mix/build/deno_integration_test.exs @@ -0,0 +1,80 @@ +defmodule Mix.Tasks.Phx.React.Deno.BundleIntegrationTest do + use ExUnit.Case, async: false + + alias Mix.Tasks.Phx.React.Deno.Bundle + + describe "bundle integration tests" do + @describetag :integration + + test "find_files handles complex directory structures" do + tmp_dir = System.tmp_dir!() + test_dir = Path.join(tmp_dir, "deno_bundle_test_#{System.unique_integer()}") + + # Create complex directory structure + File.mkdir_p!(test_dir) + File.mkdir_p!(Path.join([test_dir, "components"])) + File.mkdir_p!(Path.join([test_dir, "components", "ui"])) + File.mkdir_p!(Path.join([test_dir, "utils"])) + + # Create various files + files = [ + {"component1.js", "export const Component = () => null;"}, + {"components/component2.js", "export const Component = () => null;"}, + {"components/ui/button.js", "export const Component = () => null;"}, + {"utils/helper.js", "export const helper = () => null;"}, + {"README.md", "# Documentation"} + ] + + for {path, content} <- files do + path_parts = [test_dir | String.split(path, "/")] + full_path = Path.join(path_parts) + File.mkdir_p!(Path.dirname(full_path)) + File.write!(full_path, content) + end + + try do + found_files = Bundle.find_files(test_dir) + + # Should find all files + assert length(found_files) >= 5 + + # Check that specific files are found + file_names = Enum.map(found_files, &Path.basename/1) + assert "component1.js" in file_names + assert "component2.js" in file_names + assert "button.js" in file_names + assert "helper.js" in file_names + assert "README.md" in file_names + after + File.rm_rf!(test_dir) + end + end + + test "find_files handles empty directory" do + tmp_dir = System.tmp_dir!() + test_dir = Path.join(tmp_dir, "empty_test_#{System.unique_integer()}") + File.mkdir_p!(test_dir) + + try do + files = Bundle.find_files(test_dir) + assert files == [] + after + File.rm_rf!(test_dir) + end + end + + test "find_files handles permission errors gracefully" do + # Test with a path that doesn't exist + non_existent = "/tmp/definitely_does_not_exist_#{System.unique_integer()}" + + files = Bundle.find_files(non_existent) + assert files == [] + end + + test "bundle task handles invalid arguments" do + assert_raise ArgumentError, ~r/component_base dir does not exist/, fn -> + Bundle.run(["--component-base", "/non/existent/path", "--output", "/tmp/output.js"]) + end + end + end +end diff --git a/test/phoenix/mix/build/deno_test.exs b/test/phoenix/mix/build/deno_test.exs new file mode 100644 index 0000000..a884c26 --- /dev/null +++ b/test/phoenix/mix/build/deno_test.exs @@ -0,0 +1,48 @@ +defmodule Mix.Tasks.Phx.React.Deno.BundleTest do + use ExUnit.Case, async: false + + alias Mix.Tasks.Phx.React.Deno.Bundle + + describe "run/1" do + test "handles missing arguments gracefully" do + # Test that the function can be called + # The actual error handling depends on the OptionParser behavior + assert is_function(&Bundle.run/1) + end + + test "find_files/1 finds all files in directory recursively" do + # Create a temporary directory structure + tmp_dir = System.tmp_dir!() + test_dir = Path.join(tmp_dir, "deno_test_#{System.unique_integer()}") + File.mkdir_p!(test_dir) + + # Create some test files + File.write!(Path.join(test_dir, "component1.js"), "export const Component = () => null;") + File.mkdir!(Path.join(test_dir, "subdir")) + + File.write!( + Path.join([test_dir, "subdir", "component2.js"]), + "export const Component = () => null;" + ) + + try do + files = Bundle.find_files(test_dir) + + # Should find both files + assert length(files) == 2 + assert Enum.any?(files, &String.ends_with?(&1, "component1.js")) + assert Enum.any?(files, &String.ends_with?(&1, "component2.js")) + after + File.rm_rf!(test_dir) + end + end + + test "find_files/1 handles non-existent directory" do + non_existent = "/tmp/non_existent_#{System.unique_integer()}" + + # Should return empty list for non-existent directory + files = Bundle.find_files(non_existent) + assert files == [] + end + end +end diff --git a/test/phoenix/mix/build/server_deno_template_test.exs b/test/phoenix/mix/build/server_deno_template_test.exs new file mode 100644 index 0000000..5d44b81 --- /dev/null +++ b/test/phoenix/mix/build/server_deno_template_test.exs @@ -0,0 +1,67 @@ +defmodule Mix.Tasks.Phx.React.Deno.ServerTemplateTest do + use ExUnit.Case, async: true + + describe "server_deno.js.eex template" do + test "template compiles correctly with sample files" do + files = [ + {"component1", "/path/to/component1.js"}, + {"component2", "/path/to/component2.js"} + ] + + base_dir = "/test/components" + + # Read and compile the template + template_path = Path.expand("../../../../lib/phoenix/mix/build/server_deno.js.eex", __DIR__) + quoted = EEx.compile_file(template_path) + + # Evaluate the template + {result, _bindings} = Code.eval_quoted(quoted, files: files, base_dir: base_dir) + + # Check that the result contains expected imports + assert String.contains?( + result, + "import { Component as __component_0 } from \"/path/to/component1.js\"" + ) + + assert String.contains?( + result, + "import { Component as __component_1 } from \"/path/to/component2.js\"" + ) + + # Check that component mapping is created + assert String.contains?(result, "__comMap[\"component1\"] = __component_0") + assert String.contains?(result, "__comMap[\"component2\"] = __component_1") + + # Check that Deno-specific imports are present + assert String.contains?(result, "import { serve } from \"https://deno.land/std") + + assert String.contains?( + result, + "import { renderToReadableStream, renderToString, renderToStaticMarkup } from \"npm:react-dom/server\"" + ) + + # Check that environment variables are used + assert String.contains?(result, "DENO_ENV") + assert String.contains?(result, "COMPONENT_BASE") + + # Check that server setup is present + assert String.contains?(result, "await serve") + assert String.contains?(result, "Deno.addSignalListener") + end + + test "template handles empty files list" do + files = [] + base_dir = "/test/components" + + template_path = Path.expand("../../../../lib/phoenix/mix/build/server_deno.js.eex", __DIR__) + quoted = EEx.compile_file(template_path) + + {result, _bindings} = Code.eval_quoted(quoted, files: files, base_dir: base_dir) + + # Should still contain the basic structure + assert String.contains?(result, "const __comMap = {};") + assert String.contains?(result, "import { serve } from") + assert String.contains?(result, "await serve") + end + end +end diff --git a/test/phoenix/react/runtime/deno_integration_test.exs b/test/phoenix/react/runtime/deno_integration_test.exs new file mode 100644 index 0000000..c41ae01 --- /dev/null +++ b/test/phoenix/react/runtime/deno_integration_test.exs @@ -0,0 +1,85 @@ +defmodule Phoenix.React.Runtime.DenoIntegrationTest do + use ExUnit.Case, async: false + + alias Phoenix.React.Runtime.Deno + + describe "integration tests" do + @describetag :integration + + test "config returns proper structure" do + config = Deno.config() + + assert is_list(config) + assert Keyword.has_key?(config, :cd) + assert Keyword.has_key?(config, :cmd) + assert Keyword.has_key?(config, :server_js) + assert Keyword.has_key?(config, :port) + assert Keyword.has_key?(config, :env) + + # Test default values + assert is_binary(config[:cd]) + assert is_binary(config[:cmd]) or is_nil(config[:cmd]) + assert is_binary(config[:server_js]) + assert is_integer(config[:port]) + assert config[:port] > 0 + assert config[:env] in [:dev, :prod] + end + + test "config respects application environment" do + custom_config = [ + cmd: "/test/deno", + server_js: "/test/server.js", + port: 9999, + env: :prod, + cd: "/test/cd" + ] + + Application.put_env(:phoenix_react_server, Deno, custom_config) + + try do + config = Deno.config() + assert config[:cmd] == "/test/deno" + assert config[:server_js] == "/test/server.js" + assert config[:port] == 9999 + assert config[:env] == :prod + assert config[:cd] == "/test/cd" + after + Application.delete_env(:phoenix_react_server, Deno) + end + end + + test "get_rendered_component handles all render methods" do + state = %Phoenix.React.Runtime{ + component_base: "/tmp/components", + render_timeout: 5000, + server_js: "/tmp/server.js", + cd: "/tmp", + runtime_port: nil + } + + # Test all render methods return error when server is not running + methods = [:render_to_string, :render_to_static_markup, :render_to_readable_stream] + + for method <- methods do + result = Deno.get_rendered_component(method, "test_component", %{}, state) + assert match?({:error, _}, result) + end + end + + test "runtime state structure is valid" do + state = %Phoenix.React.Runtime{ + component_base: "/test/components", + render_timeout: 30000, + server_js: "/test/server.js", + cd: "/test", + runtime_port: nil + } + + assert state.component_base == "/test/components" + assert state.render_timeout == 30000 + assert state.server_js == "/test/server.js" + assert state.cd == "/test" + assert state.runtime_port == nil + end + end +end diff --git a/test/phoenix/react/runtime/deno_test.exs b/test/phoenix/react/runtime/deno_test.exs new file mode 100644 index 0000000..ea06e21 --- /dev/null +++ b/test/phoenix/react/runtime/deno_test.exs @@ -0,0 +1,72 @@ +defmodule Phoenix.React.Runtime.DenoTest do + use ExUnit.Case, async: false + + alias Phoenix.React.Runtime.Deno + + describe "config/0" do + test "returns default configuration" do + config = Deno.config() + + assert Keyword.keyword?(config) + assert Keyword.has_key?(config, :cd) + assert Keyword.has_key?(config, :cmd) + assert Keyword.has_key?(config, :server_js) + assert Keyword.has_key?(config, :port) + assert Keyword.has_key?(config, :env) + end + + test "uses custom configuration when provided" do + Application.put_env(:phoenix_react_server, Deno, + cmd: "/custom/deno", + server_js: "/custom/server.js", + port: 9999, + env: :prod + ) + + try do + config = Deno.config() + assert config[:cmd] == "/custom/deno" + assert config[:server_js] == "/custom/server.js" + assert config[:port] == 9999 + assert config[:env] == :prod + after + Application.delete_env(:phoenix_react_server, Deno) + end + end + end + + describe "start/1" do + test "starts deno process with correct arguments" do + # This test would require a actual deno installation + # For now, we'll test the configuration part + _component_base = "/tmp/components" + + # Mock the Port.open to avoid actually starting deno + # In a real test environment, you might want to use Mox or similar + + # Test that the function exists and can be called + assert is_function(&Deno.start/1) + end + end + + describe "get_rendered_component/4" do + test "returns error when server is not running" do + component = "test_component" + props = %{"test" => "value"} + + state = %Phoenix.React.Runtime{ + component_base: "/tmp/components", + render_timeout: 5000, + server_js: "/tmp/server.js", + cd: "/tmp", + runtime_port: nil + } + + # This should fail since there's no server running + result = Deno.get_rendered_component(:render_to_string, component, props, state) + + # The result should be an error tuple + assert match?({:error, _}, result) + end + end +end diff --git a/test/phoenix/react/runtime_integration_test.exs b/test/phoenix/react/runtime_integration_test.exs new file mode 100644 index 0000000..e31d576 --- /dev/null +++ b/test/phoenix/react/runtime_integration_test.exs @@ -0,0 +1,230 @@ +defmodule Phoenix.React.RuntimeIntegrationTest do + use ExUnit.Case, async: false + + alias Phoenix.React.Runtime.Bun + alias Phoenix.React.Runtime.Deno + alias Phoenix.React.Config + + @moduletag :integration + + setup do + # Clean up any existing runtimes before each test + on_exit(fn -> + # Give processes time to clean up + Process.sleep(100) + end) + + :ok + end + + describe "Bun Runtime Integration" do + @describetag :bun_runtime + + test "configuration validation" do + # Test valid configuration + valid_config = [ + cmd: "bun", + port: 5225, + env: :dev, + cd: File.cwd!() + ] + + assert {:ok, config} = Config.runtime_config(:bun, Enum.into(valid_config, %{})) + assert config.cmd == "bun" + assert config.port == 5225 + assert config.env == :dev + + # Test invalid configuration + invalid_config = [ + cmd: "bun", + # Invalid port + port: 70000, + env: :dev + ] + + assert {:error, _} = Config.runtime_config(:bun, Enum.into(invalid_config, %{})) + end + + test "runtime startup and shutdown" do + # Skip if bun is not available + unless System.find_executable("bun") do + flunk("Bun not available for integration testing") + end + + # Try multiple times to find an available port + {_test_port, pid} = + Enum.reduce_while(1..5, nil, fn _attempt, _acc -> + test_port = 15225 + :rand.uniform(1000) + + Application.put_env(:phoenix_react_server, Bun, + cmd: System.find_executable("bun"), + port: test_port, + env: :dev, + cd: File.cwd!() + ) + + # Start the runtime without name registration + case GenServer.start_link(Bun, component_base: "test/fixtures", render_timeout: 5000) do + {:ok, pid} -> + # Give it time to start + Process.sleep(2000) + + if Process.alive?(pid) do + {:halt, {test_port, pid}} + else + # Process died, try another port + {:cont, nil} + end + + {:error, _reason} -> + # Failed to start, try another port + {:cont, nil} + end + end) + + # If we couldn't find a working port, fail the test + if pid == nil do + flunk("Could not start Bun runtime after 5 attempts") + end + + # Verify it's running + assert Process.alive?(pid) + + # Stop the runtime + GenServer.stop(pid, :normal) + + # Verify it's stopped + refute Process.alive?(pid) + end + end + + describe "Deno Runtime Integration" do + @describetag :deno_runtime + @describetag :skip_if_no_deno + + test "configuration validation" do + # Test valid configuration + valid_config = %{ + cmd: "deno", + port: 5226, + env: :dev, + write_dirs: ["/tmp"], + parent_check_interval: 5000 + } + + assert {:ok, config} = Config.runtime_config(:deno, valid_config) + assert config.cmd == "deno" + assert config.port == 5226 + assert config.write_dirs == ["/tmp"] + + # Test invalid configuration + invalid_config = %{ + cmd: "deno", + # Invalid port + port: 70000, + env: :dev, + # Empty write_dirs + write_dirs: [] + } + + assert {:error, _} = Config.runtime_config(:deno, invalid_config) + end + + @tag :skip_if_no_deno + test "runtime startup and shutdown" do + # Skip if deno is not available - this is handled by the setup below + + # Configure test environment with unique port + test_port = 15227 + :rand.uniform(1000) + + Application.put_env(:phoenix_react_server, Deno, + cmd: System.find_executable("deno"), + port: test_port, + env: :dev, + cd: File.cwd!(), + write_dirs: ["/tmp"], + parent_check_interval: 2000 + ) + + # Start the runtime + {:ok, pid} = Deno.start_link(component_base: "test/fixtures", render_timeout: 5000) + + # Give it time to start + Process.sleep(3000) + + # Verify it's running + assert Process.alive?(pid) + + # Stop the runtime + GenServer.stop(pid, :normal) + + # Verify it's stopped + refute Process.alive?(pid) + end + end + + describe "Common Functionality" do + test "security configuration validation" do + security_config = Config.security_config() + + assert security_config.max_component_name_length == 100 + assert is_struct(security_config.allowed_component_name_pattern, Regex) + assert security_config.max_request_size == 1_048_576 + assert security_config.request_timeout_ms == 30000 + end + + test "file watcher configuration validation" do + watcher_config = Config.file_watcher_config() + + assert watcher_config.throttle_ms == 3000 + assert watcher_config.debounce_ms == 100 + end + + test "monitoring functionality" do + # Test monitoring functions don't crash + assert :ok = Phoenix.React.Monitoring.record_render("test", :render_to_string, 100, :ok) + assert :ok = Phoenix.React.Monitoring.record_runtime_startup("test", 5225) + assert :ok = Phoenix.React.Monitoring.record_runtime_shutdown("test", :normal) + assert :ok = Phoenix.React.Monitoring.record_file_change("/test/path", "changed") + assert :ok = Phoenix.React.Monitoring.record_build("test", 1000, :ok) + + # Test measurement function + result = + Phoenix.React.Monitoring.measure("test_op", [:test], fn -> + Process.sleep(10) + :test_result + end) + + assert result == :test_result + + # Test runtime stats + stats = Phoenix.React.Monitoring.get_runtime_stats("test") + assert is_map(stats) + assert stats.runtime == "test" + end + end + + describe "Error Handling" do + test "invalid component names are rejected" do + # This would be tested through the actual HTTP requests to the runtime servers + # For now, we test the validation logic + security_config = Config.security_config() + pattern = security_config.allowed_component_name_pattern + + # Valid names + assert "valid_component" =~ pattern + assert "valid-component-123" =~ pattern + + # Invalid names + refute "../../../etc/passwd" =~ pattern + refute "component