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 = `
+
+
+
${escapeHtml(error.toString())}
+
${escapeHtml(error.stack || '')}
+
+
+ `;
+ 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 = `
+
+
+
${escapeHtml(error.toString())}
+
${escapeHtml(error.stack || '')}
+
+
+ `;
+ 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 = `
+
+
+
${escapeHtml(error.toString())}
+
${escapeHtml(error.stack || '')}
+
+
+ `;
+ 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