From d29608d404fd7b9d8a05ecde97eef2ab5047bbf9 Mon Sep 17 00:00:00 2001 From: Promger <96533520+thePromger@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:18:21 +0530 Subject: [PATCH 001/106] ci: refactor and clean codspeed ci (#1230) * Removed UV_SYSTEM_PYTHON env, coz no need for that. * Using uv sync with venv to install all dependencies + project * Removed macos target, coz its running on ubuntu server * Cleaned up unnecessary code * Some speed improvements --- .github/workflows/codspeed.yml | 39 ++++++++-------------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 6ccdfe558..916b4a960 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -9,8 +9,6 @@ on: # performance analysis in order to generate initial data. workflow_dispatch: -env: - UV_SYSTEM_PYTHON: 1 jobs: benchmarks: @@ -18,43 +16,24 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.9" + - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.7.5" - - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install dependencies with uv - run: | - echo "# Installing Dependencies" - uv pip install -r pyproject.toml --group dev --group test - echo "# Installing Project" - uv pip install -e . - - - name: Add macos target - if: matrix.os == 'macos' - run: rustup target add aarch64-apple-darwin - - - name: Setup Rust part of the project + - name: Install the project + deps. run: | - echo "::group::Checking dependencies" - echo "# Checking dependencies, If not found any then it will stop the job." - which uv python pip maturin pytest - echo "# Checking pip list" - pip list - echo "::endgroup::" - - echo "::group::Running & Installing build" - maturin build -i python --universal2 --out dist - uv pip install --no-index --find-links=dist/ robyn - echo "::endgroup::" + echo "# Syncing Project + Installing project." + uv sync --dev --group test --verbose - name: Run benchmarks uses: CodSpeedHQ/action@v3.5.0 with: token: ${{ secrets.CODSPEED_TOKEN }} - run: pytest integration_tests --codspeed + run: uv run pytest integration_tests --codspeed From 74f686f491e5dc7b0057cb16d85dd2f4e801e973 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sat, 30 Aug 2025 01:18:58 +0100 Subject: [PATCH 002/106] Release 0.72.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c9a6a1d6..d00f784b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,7 +1516,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.72.0" +version = "0.72.1" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 99e788897..bed7e56ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.72.0" +version = "0.72.1" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index b853097e0..0e7c2c3db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.72.0" +version = "0.72.1" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] license = { file = "LICENSE" } @@ -66,7 +66,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.72.0" +version = "0.72.1" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From c82be709d59cc4b9923291045a6d67925801d3d2 Mon Sep 17 00:00:00 2001 From: Armin Stepanyan Date: Sat, 30 Aug 2025 01:29:00 +0100 Subject: [PATCH 003/106] chore: create Recurse ML rules (#1214) * Create rules * Don't specify explicit pathnames for effective comments --- .recurseml.yaml | 1 + .rules/effective_comments.mdc | 23 +++++++++ .rules/exhaustive_pattern_matching.mdc | 48 +++++++++++++++++++ .rules/proper_error_handling.mdc | 64 ++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 .recurseml.yaml create mode 100644 .rules/effective_comments.mdc create mode 100644 .rules/exhaustive_pattern_matching.mdc create mode 100644 .rules/proper_error_handling.mdc diff --git a/.recurseml.yaml b/.recurseml.yaml new file mode 100644 index 000000000..92f04df80 --- /dev/null +++ b/.recurseml.yaml @@ -0,0 +1 @@ +rules: .rules/ diff --git a/.rules/effective_comments.mdc b/.rules/effective_comments.mdc new file mode 100644 index 000000000..86d2cd997 --- /dev/null +++ b/.rules/effective_comments.mdc @@ -0,0 +1,23 @@ +--- +description: Effective Code Comments +globs: "*" +alwaysApply: true +--- + + +For more context read: https://blog.codinghorror.com/code-tells-you-how-comments-tell-you-why/ + +- NEVER write comments for code that can be understood from the code itself +- Avoid redundant comments that restate the obvious +- Use comments to clarify the intent behind complex logic or decisions +- Use comments when non-obvious assumptions are made +- Keep TODO comments specific with assignees when possible +- ALWAYS update comments when the underlying code changes + + +SCOPE: this only applies to code comments blocks NOT executable code (such as logging statements) + +# Solution + +Fix redundant comments by removing them completely. +Never attempt to add additional clarification to the comments. diff --git a/.rules/exhaustive_pattern_matching.mdc b/.rules/exhaustive_pattern_matching.mdc new file mode 100644 index 000000000..0821f855e --- /dev/null +++ b/.rules/exhaustive_pattern_matching.mdc @@ -0,0 +1,48 @@ +--- +description: Exhaustive Pattern Matching with match +globs: "**/*.rs" +alwaysApply: true +--- + +For more context read: https://doc.rust-lang.org/book/ch06-02-match.html + +- ALWAYS use `match` instead of nested `if-else` chains for enum handling +- Use `match` for exhaustive pattern matching to prevent runtime panics +- Add match guards (`if` conditions) for conditional patterns within matches +- Handle all variants explicitly - avoid catch-all patterns when possible +- Use `match` for HTTP methods, response types, and middleware returns in Robyn +- Pattern match on `Option` and `Result` types instead of using `unwrap()` + +SCOPE: This applies to Rust enum handling, especially for HttpMethod, ResponseType, and MiddlewareReturn types in the Robyn codebase. + +# Solution + +Replace nested if-else chains with match statements: + +```rust +// Instead of: +if method == HttpMethod::GET { + handle_get() +} else if method == HttpMethod::POST { + handle_post() +} else { + handle_other() +} + +// Use: +match method { + HttpMethod::GET => handle_get(), + HttpMethod::POST => handle_post(), + _ => handle_other(), +} +``` + +Use match guards for conditional logic: + +```rust +match response_type { + ResponseType::Standard(resp) if resp.status_code >= 400 => handle_error(resp), + ResponseType::Standard(resp) => handle_success(resp), + ResponseType::Streaming(stream) => handle_stream(stream), +} +``` \ No newline at end of file diff --git a/.rules/proper_error_handling.mdc b/.rules/proper_error_handling.mdc new file mode 100644 index 000000000..6f973b8b1 --- /dev/null +++ b/.rules/proper_error_handling.mdc @@ -0,0 +1,64 @@ +--- +description: Proper Error Handling with Result and ? Operator +globs: "**/*.rs" +alwaysApply: true +--- + +For more context read: https://doc.rust-lang.org/book/ch09-00-error-handling.html + +- NEVER use `unwrap()` or `expect()` in production code without clear justification +- Use the `?` operator for error propagation in functions returning `Result` +- Handle `Result` and `Option` types explicitly with `match` or combinators +- Create custom error types for domain-specific errors in Robyn +- Use `map_err()` to transform errors when interfacing with PyO3 +- Log errors at appropriate levels before returning them +- Return meaningful error messages for HTTP endpoints + +WHY THIS MATTERS IN ROBYN: +Robyn is a high-performance web framework that interfaces between Python and Rust. Poor error handling can: +- Crash the entire web server with a single panic +- Lose critical error information when interfacing with Python +- Provide unhelpful error messages to API consumers +- Make debugging production issues nearly impossible +- Break the Python-Rust bridge unexpectedly + +SCOPE: This applies to all Rust error handling, especially PyO3 integration, file I/O, and HTTP request processing in the Robyn codebase. + +# Solution + +Replace unwrap() with proper error handling: + +```rust +// Instead of: +let value = some_operation().unwrap(); + +// Use: +let value = some_operation()?; +// Or handle explicitly: +let value = match some_operation() { + Ok(val) => val, + Err(e) => { + log::error!("Operation failed: {}", e); + return Err(format!("Operation failed: {}", e)); + } +}; +``` + +Use map_err for PyO3 integration: + +```rust +// Transform errors when interfacing with Python +let result = python_operation() + .map_err(|e| format!("Python operation failed: {}", e))?; +``` + +Handle file operations safely: + +```rust +// Instead of: +let content = std::fs::read_to_string(path).unwrap(); + +// Use: +let content = std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read file {}: {}", path, e))?; +``` \ No newline at end of file From a77063a8473ca62d8c7deebd7635b03e46b47af1 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:15:26 +0100 Subject: [PATCH 004/106] fix: breaking on unsupported methods (#1241) * fix: breaking on unsupported methods * update * fix formatting --- src/server.rs | 21 +++++++++++++-------- src/types/mod.rs | 37 ++++++++++++++++++++++++++----------- src/types/response.rs | 12 ++++++++++++ 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/server.rs b/src/server.rs index 2479c45c1..04b366003 100644 --- a/src/server.rs +++ b/src/server.rs @@ -466,6 +466,11 @@ async fn index( excluded_response_headers_paths: web::Data>>, req: HttpRequest, ) -> ResponseType { + // Check if the HTTP method is supported + if !HttpMethod::is_supported(req.method()) { + return ResponseType::Standard(Response::method_not_allowed(None)); + } + let mut request = Request::from_actix_request(&req, payload, &global_request_headers).await; let route = format!("{}{}", req.method(), req.uri().path()); @@ -500,15 +505,15 @@ async fn index( } // Route execution - let mut response = if let Some(res) = const_router.get_route( - &HttpMethod::from_actix_method(req.method()), - req.uri().path(), - ) { + let http_method = match HttpMethod::from_actix_method(req.method()) { + Ok(method) => method, + Err(_) => return ResponseType::Standard(Response::method_not_allowed(None)), + }; + + let mut response = if let Some(res) = const_router.get_route(&http_method, req.uri().path()) { ResponseType::Standard(res) - } else if let Some((function, route_params)) = router.get_route( - &HttpMethod::from_actix_method(req.method()), - req.uri().path(), - ) { + } else if let Some((function, route_params)) = router.get_route(&http_method, req.uri().path()) + { request.path_params = route_params; match execute_http_function(&request, &function).await { Ok(r) => r, diff --git a/src/types/mod.rs b/src/types/mod.rs index 3ed9bd7e1..2fd1b6b8d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -34,18 +34,33 @@ pub enum HttpMethod { } impl HttpMethod { - pub fn from_actix_method(method: &actix_web::http::Method) -> Self { + pub fn is_supported(method: &actix_web::http::Method) -> bool { + matches!( + *method, + actix_web::http::Method::GET + | actix_web::http::Method::POST + | actix_web::http::Method::PUT + | actix_web::http::Method::DELETE + | actix_web::http::Method::PATCH + | actix_web::http::Method::HEAD + | actix_web::http::Method::OPTIONS + | actix_web::http::Method::CONNECT + | actix_web::http::Method::TRACE + ) + } + + pub fn from_actix_method(method: &actix_web::http::Method) -> Result { match *method { - actix_web::http::Method::GET => Self::GET, - actix_web::http::Method::POST => Self::POST, - actix_web::http::Method::PUT => Self::PUT, - actix_web::http::Method::DELETE => Self::DELETE, - actix_web::http::Method::PATCH => Self::PATCH, - actix_web::http::Method::HEAD => Self::HEAD, - actix_web::http::Method::OPTIONS => Self::OPTIONS, - actix_web::http::Method::CONNECT => Self::CONNECT, - actix_web::http::Method::TRACE => Self::TRACE, - _ => panic!("Unsupported HTTP method"), + actix_web::http::Method::GET => Ok(Self::GET), + actix_web::http::Method::POST => Ok(Self::POST), + actix_web::http::Method::PUT => Ok(Self::PUT), + actix_web::http::Method::DELETE => Ok(Self::DELETE), + actix_web::http::Method::PATCH => Ok(Self::PATCH), + actix_web::http::Method::HEAD => Ok(Self::HEAD), + actix_web::http::Method::OPTIONS => Ok(Self::OPTIONS), + actix_web::http::Method::CONNECT => Ok(Self::CONNECT), + actix_web::http::Method::TRACE => Ok(Self::TRACE), + _ => Err("Method Not Allowed"), } } } diff --git a/src/types/response.rs b/src/types/response.rs index d8b9e7809..a439aa889 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -193,6 +193,18 @@ impl Response { file_path: None, } } + + pub fn method_not_allowed(headers: Option<&Headers>) -> Self { + const METHOD_NOT_ALLOWED_BYTES: &[u8] = b"Method not allowed"; + + Self { + status_code: 405, + response_type: "text".to_string(), + headers: headers.cloned().unwrap_or_else(|| Headers::new(None)), + description: METHOD_NOT_ALLOWED_BYTES.to_vec(), + file_path: None, + } + } } impl<'py> IntoPyObject<'py> for Response { From ffb7ebc1e9abfc7767649985a2edb6ca81b2e87f Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sat, 6 Sep 2025 09:25:04 +0530 Subject: [PATCH 005/106] Release 0.72.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d00f784b7..15e37f833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,7 +1516,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.72.1" +version = "0.72.2" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index bed7e56ff..eb7cb4857 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.72.1" +version = "0.72.2" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index 0e7c2c3db..e223e3262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.72.1" +version = "0.72.2" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] license = { file = "LICENSE" } @@ -66,7 +66,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.72.1" +version = "0.72.2" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 118660f4b6c3ec955857f6104f73500d24b5ab36 Mon Sep 17 00:00:00 2001 From: Promger <96533520+thePromger@users.noreply.github.com> Date: Tue, 23 Sep 2025 20:43:47 +0530 Subject: [PATCH 006/106] docs: add DeepWiki badge to README (#1248) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d90218370..aa0f3f8a8 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ [![view - Documentation](https://img.shields.io/badge/view-Documentation-blue?style=for-the-badge)](https://robyn.tech/documentation) [![Discord](https://img.shields.io/discord/999782964143603713?label=discord&logo=discord&logoColor=white&style=for-the-badge&color=blue)](https://discord.gg/rkERZ5eNU8) [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Robyn%20Guru-006BFF?style=for-the-badge)](https://gurubase.io/g/robyn) +Ask DeepWiki Robyn is a High-Performance, Community-Driven, and Innovator Friendly Web Framework with a Rust runtime. You can learn more by checking our [community resources](https://robyn.tech/documentation/en/community-resources#talks)! From 40a8bd115f890e86b850c323b1738532bf577fc3 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sat, 18 Oct 2025 13:49:48 +0100 Subject: [PATCH 007/106] chore: remove recurse ml integration --- .recurseml.yaml | 1 - .rules/effective_comments.mdc | 23 --------- .rules/exhaustive_pattern_matching.mdc | 48 ------------------- .rules/proper_error_handling.mdc | 64 -------------------------- 4 files changed, 136 deletions(-) delete mode 100644 .recurseml.yaml delete mode 100644 .rules/effective_comments.mdc delete mode 100644 .rules/exhaustive_pattern_matching.mdc delete mode 100644 .rules/proper_error_handling.mdc diff --git a/.recurseml.yaml b/.recurseml.yaml deleted file mode 100644 index 92f04df80..000000000 --- a/.recurseml.yaml +++ /dev/null @@ -1 +0,0 @@ -rules: .rules/ diff --git a/.rules/effective_comments.mdc b/.rules/effective_comments.mdc deleted file mode 100644 index 86d2cd997..000000000 --- a/.rules/effective_comments.mdc +++ /dev/null @@ -1,23 +0,0 @@ ---- -description: Effective Code Comments -globs: "*" -alwaysApply: true ---- - - -For more context read: https://blog.codinghorror.com/code-tells-you-how-comments-tell-you-why/ - -- NEVER write comments for code that can be understood from the code itself -- Avoid redundant comments that restate the obvious -- Use comments to clarify the intent behind complex logic or decisions -- Use comments when non-obvious assumptions are made -- Keep TODO comments specific with assignees when possible -- ALWAYS update comments when the underlying code changes - - -SCOPE: this only applies to code comments blocks NOT executable code (such as logging statements) - -# Solution - -Fix redundant comments by removing them completely. -Never attempt to add additional clarification to the comments. diff --git a/.rules/exhaustive_pattern_matching.mdc b/.rules/exhaustive_pattern_matching.mdc deleted file mode 100644 index 0821f855e..000000000 --- a/.rules/exhaustive_pattern_matching.mdc +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: Exhaustive Pattern Matching with match -globs: "**/*.rs" -alwaysApply: true ---- - -For more context read: https://doc.rust-lang.org/book/ch06-02-match.html - -- ALWAYS use `match` instead of nested `if-else` chains for enum handling -- Use `match` for exhaustive pattern matching to prevent runtime panics -- Add match guards (`if` conditions) for conditional patterns within matches -- Handle all variants explicitly - avoid catch-all patterns when possible -- Use `match` for HTTP methods, response types, and middleware returns in Robyn -- Pattern match on `Option` and `Result` types instead of using `unwrap()` - -SCOPE: This applies to Rust enum handling, especially for HttpMethod, ResponseType, and MiddlewareReturn types in the Robyn codebase. - -# Solution - -Replace nested if-else chains with match statements: - -```rust -// Instead of: -if method == HttpMethod::GET { - handle_get() -} else if method == HttpMethod::POST { - handle_post() -} else { - handle_other() -} - -// Use: -match method { - HttpMethod::GET => handle_get(), - HttpMethod::POST => handle_post(), - _ => handle_other(), -} -``` - -Use match guards for conditional logic: - -```rust -match response_type { - ResponseType::Standard(resp) if resp.status_code >= 400 => handle_error(resp), - ResponseType::Standard(resp) => handle_success(resp), - ResponseType::Streaming(stream) => handle_stream(stream), -} -``` \ No newline at end of file diff --git a/.rules/proper_error_handling.mdc b/.rules/proper_error_handling.mdc deleted file mode 100644 index 6f973b8b1..000000000 --- a/.rules/proper_error_handling.mdc +++ /dev/null @@ -1,64 +0,0 @@ ---- -description: Proper Error Handling with Result and ? Operator -globs: "**/*.rs" -alwaysApply: true ---- - -For more context read: https://doc.rust-lang.org/book/ch09-00-error-handling.html - -- NEVER use `unwrap()` or `expect()` in production code without clear justification -- Use the `?` operator for error propagation in functions returning `Result` -- Handle `Result` and `Option` types explicitly with `match` or combinators -- Create custom error types for domain-specific errors in Robyn -- Use `map_err()` to transform errors when interfacing with PyO3 -- Log errors at appropriate levels before returning them -- Return meaningful error messages for HTTP endpoints - -WHY THIS MATTERS IN ROBYN: -Robyn is a high-performance web framework that interfaces between Python and Rust. Poor error handling can: -- Crash the entire web server with a single panic -- Lose critical error information when interfacing with Python -- Provide unhelpful error messages to API consumers -- Make debugging production issues nearly impossible -- Break the Python-Rust bridge unexpectedly - -SCOPE: This applies to all Rust error handling, especially PyO3 integration, file I/O, and HTTP request processing in the Robyn codebase. - -# Solution - -Replace unwrap() with proper error handling: - -```rust -// Instead of: -let value = some_operation().unwrap(); - -// Use: -let value = some_operation()?; -// Or handle explicitly: -let value = match some_operation() { - Ok(val) => val, - Err(e) => { - log::error!("Operation failed: {}", e); - return Err(format!("Operation failed: {}", e)); - } -}; -``` - -Use map_err for PyO3 integration: - -```rust -// Transform errors when interfacing with Python -let result = python_operation() - .map_err(|e| format!("Python operation failed: {}", e))?; -``` - -Handle file operations safely: - -```rust -// Instead of: -let content = std::fs::read_to_string(path).unwrap(); - -// Use: -let content = std::fs::read_to_string(path) - .map_err(|e| format!("Failed to read file {}: {}", path, e))?; -``` \ No newline at end of file From 94d5009cdcebfd80729d1a9805fd5beb88a6ac5e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 11 Dec 2025 20:26:04 +0300 Subject: [PATCH 008/106] docs: Fix badges in the README (#1264) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa0f3f8a8..eccb7517a 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ [![GitHub tag](https://img.shields.io/github/tag/sparckles/Robyn?include_prereleases=&sort=semver&color=black)](https://github.com/sparckles/Robyn/releases/) [![License](https://img.shields.io/badge/License-BSD_2.0-black)](https://github.com/sparckles/Robyn/blob/main/LICENSE) ![Python](https://img.shields.io/badge/Support-Version%20%E2%89%A5%203.9-brightgreen) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/sparckles/Robyn) [![view - Documentation](https://img.shields.io/badge/view-Documentation-blue?style=for-the-badge)](https://robyn.tech/documentation) [![Discord](https://img.shields.io/discord/999782964143603713?label=discord&logo=discord&logoColor=white&style=for-the-badge&color=blue)](https://discord.gg/rkERZ5eNU8) [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Robyn%20Guru-006BFF?style=for-the-badge)](https://gurubase.io/g/robyn) -Ask DeepWiki Robyn is a High-Performance, Community-Driven, and Innovator Friendly Web Framework with a Rust runtime. You can learn more by checking our [community resources](https://robyn.tech/documentation/en/community-resources#talks)! From 3a3a19a1f896a7991555d3010c7b5bf04f35df01 Mon Sep 17 00:00:00 2001 From: Promger <96533520+thePromger@users.noreply.github.com> Date: Sun, 14 Dec 2025 16:11:23 +0530 Subject: [PATCH 009/106] fix: out of order bug in websocket tests (#1261) * This fixes, assert error occurred randomly in websocket test, due to getting data not in same order as sent from server --- integration_tests/test_web_sockets.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/integration_tests/test_web_sockets.py b/integration_tests/test_web_sockets.py index 65c9f0fdf..da3d3ee9d 100644 --- a/integration_tests/test_web_sockets.py +++ b/integration_tests/test_web_sockets.py @@ -12,17 +12,18 @@ def test_web_socket_raw_benchmark(session): assert ws.recv() == "Hello world, from ws" ws.send("My name is?") - assert ws.recv() == "This is a broadcast message" - assert ws.recv() == "This is a message to self" - assert ws.recv() == "Whaaat??" + # Messages may arrive in any order due to WebSocket broadcast behavior + received = sorted([ws.recv() for _ in range(3)]) + expected = sorted(["This is a broadcast message", "This is a message to self", "Whaaat??"]) + assert received == expected ws.send("My name is?") assert ws.recv() == "Whooo??" ws.send("My name is?") - assert ws.recv() == "hi" - assert ws.recv() == "hello" - assert ws.recv() == "*chika* *chika* Slim Shady." + received = sorted([ws.recv() for _ in range(3)]) + expected = sorted(["hi", "hello", "*chika* *chika* Slim Shady."]) + assert received == expected # this will close the connection ws.send("test") From d84987382e48d3afc468bf2de08eaf5598e75106 Mon Sep 17 00:00:00 2001 From: Promger <96533520+thePromger@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:25:47 +0530 Subject: [PATCH 010/106] fix: fixing routing, while files & api served from same base route (#1260) * fix: fixing routing to serve static files & api routes from same route * bug-fix: add method guard to allow http methods in File Service in actix * fix: enhance file serving logic to ensure only regular files are accessible * feat: added integration_tests & small bug in file service route handling. * fix: a small bug in guard in file service while making a resolving the path for a file. * feat: Fixing isort errors * isort format can be applied, using isort test.py * chore: rename file serving handler for clarity and improve docstring * fix: reorder static file serving logic in main function for clarity --- integration_tests/base_routes.py | 8 +++- integration_tests/subroutes/__init__.py | 3 +- integration_tests/subroutes/file_api.py | 25 +++++++++++++ .../test_static_files_with_api_routes.py | 33 +++++++++++++++++ src/server.rs | 37 +++++++++++++------ 5 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 integration_tests/subroutes/file_api.py create mode 100644 integration_tests/test_static_files_with_api_routes.py diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 42e8064ba..128f1a057 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -6,7 +6,7 @@ from collections import defaultdict from typing import Optional -from integration_tests.subroutes import di_subrouter, sub_router +from integration_tests.subroutes import di_subrouter, static_router, sub_router from robyn import Headers, Request, Response, Robyn, SSEMessage, SSEResponse, WebSocket, WebSocketConnector, jsonify, serve_file, serve_html from robyn.authentication import AuthenticationHandler, BearerGetter, Identity from robyn.robyn import QueryParams, Url @@ -1211,9 +1211,15 @@ def main(): directory_path=os.path.join(current_file_path, "build"), index_file="index.html", ) + # Serving static files at /static from ./integration_tests. + app.serve_directory( + route="/static", + directory_path=str(current_file_path), + ) app.startup_handler(startup_handler) app.include_router(sub_router) app.include_router(di_subrouter) + app.include_router(static_router) class BasicAuthHandler(AuthenticationHandler): def authenticate(self, request: Request) -> Optional[Identity]: diff --git a/integration_tests/subroutes/__init__.py b/integration_tests/subroutes/__init__.py index 3ebab1106..ec10298c1 100644 --- a/integration_tests/subroutes/__init__.py +++ b/integration_tests/subroutes/__init__.py @@ -1,12 +1,13 @@ from robyn import SubRouter, WebSocket, jsonify from .di_subrouter import di_subrouter +from .file_api import static_router sub_router = SubRouter(__name__, prefix="/sub_router") websocket = WebSocket(sub_router, "/ws") -__all__ = ["sub_router", "websocket", "di_subrouter"] +__all__ = ["sub_router", "websocket", "di_subrouter", "static_router"] @websocket.on("connect") diff --git a/integration_tests/subroutes/file_api.py b/integration_tests/subroutes/file_api.py new file mode 100644 index 000000000..121721bec --- /dev/null +++ b/integration_tests/subroutes/file_api.py @@ -0,0 +1,25 @@ +""" +Test routes for issue #1251: static files + API routes at same base path. + +Notes: +1. No need to test every method, just one is enough to ensure no conflict. +2. The static files are served from ./integration_tests to avoid conflict with the /test_dir route in main app. +3. The static file serving route is defined in a separate SubRouter to isolate it from the main app routes. +4. Serving api & files from the same /static path to test the fix. +""" + +from robyn import Request, SubRouter + +static_router = SubRouter(__name__, "/static") + + +@static_router.get("/build") +@static_router.post("/build") +async def build_handler(request: Request): + """ + Test route ensuring no conflict with static file serving at /static. + + Although static files are served at /static, this API route should be reached + because /build is not a file, so the request falls through to the API handler. + """ + return f"{request.method}:{request.url.path} works" diff --git a/integration_tests/test_static_files_with_api_routes.py b/integration_tests/test_static_files_with_api_routes.py new file mode 100644 index 000000000..b50d22f41 --- /dev/null +++ b/integration_tests/test_static_files_with_api_routes.py @@ -0,0 +1,33 @@ +""" +Test for issue #1251: Verify that API routes work correctly +when static files are served from the same base path. + +This test ensures that non-GET/HEAD HTTP methods properly fall through +to API handlers when a static file service is mounted at the same route. +""" + +import pytest + +from integration_tests.helpers.http_methods_helpers import get, post + +# Notes: +# 1. The /static route serves the integration_tests having files & directories. + + +@pytest.mark.benchmark +def test_post_api_route_with_root_static_files(session): + """Test that POST requests reach API handlers (issue #1251). + This ensures that non-GET/HEAD methods are not blocked by static file serving. + """ + response = post("/static/build") + assert response.status_code == 200 + assert response.text == f"{response.request.method}:{response.request.path_url} works" + + +@pytest.mark.benchmark +def test_static_file_still_served_correctly(session): + """Verify that actual static files are still served correctly.""" + response = get("/static/build/index.html", should_check_response=False) + assert response.status_code == 200 + # Should serve the index.html file + assert "html" in response.text.lower() diff --git a/src/server.rs b/src/server.rs index 04b366003..f0a7ecee7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -152,22 +152,35 @@ impl Server { // 2. Shows file listing // 3. Just serves the file without any redirection to sub links for directory in directories.iter() { + let mut files = Files::new(&directory.route, &directory.directory_path) + .method_guard(guard::fn_guard(|_| true)) + .redirect_to_slash_directory(); if let Some(index_file) = &directory.index_file { - app = app.service( - Files::new(&directory.route, &directory.directory_path) - .index_file(index_file) - .redirect_to_slash_directory(), - ); + files = files.index_file(index_file); } else if directory.show_files_listing { - app = app.service( - Files::new(&directory.route, &directory.directory_path) - .redirect_to_slash_directory() - .show_files_listing(), - ); + files = files.show_files_listing(); } else { - app = app - .service(Files::new(&directory.route, &directory.directory_path)); + // To serve regular files only, nothing else. + let directory_path = directory.directory_path.clone(); + let directory_route = directory.route.clone(); + // This guard allows request if it corresponds to a regular file. + files = files.guard(guard::fn_guard(move |ctx| { + let route = ctx.head().uri.path(); + // Resolve the path by combining directory path and requested path + let full_path = std::path::Path::new(&directory_path).join( + route + .trim_start_matches(&directory_route) + .trim_start_matches("/"), + ); + // Check if the path exists and is a regular file (not dir/symlink) + if let Ok(metadata) = std::fs::metadata(&full_path) { + metadata.is_file() + } else { + false + } + })); } + app = app.service(files); } app = app From 9865bf44591e91d24f17f3b224964ff7347f8592 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sat, 10 Jan 2026 15:44:03 +0000 Subject: [PATCH 011/106] Release 0.73.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15e37f833..839753c12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,7 +1516,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.72.2" +version = "0.73.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index eb7cb4857..460893541 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.72.2" +version = "0.73.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index e223e3262..c2860e2ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.72.2" +version = "0.73.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] license = { file = "LICENSE" } @@ -66,7 +66,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.72.2" +version = "0.73.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 119459a750b58df69828b1e8d5eaae0dfc96ab14 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:30:39 +0000 Subject: [PATCH 012/106] chore: support python 3.14 (#1271) * chore: support python 3.14 * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/codspeed.yml | 4 +- .github/workflows/lint-pr.yml | 2 +- .github/workflows/preview-deployments.yml | 49 +- .github/workflows/python-CI.yml | 4 +- .github/workflows/release-CI.yml | 25 +- Cargo.lock | 38 +- Cargo.toml | 10 +- README.md | 8 +- .../test_dependency_injection.py | 8 +- noxfile.py | 2 +- poetry.lock | 469 +++++++++--------- pyproject.toml | 39 +- robyn/__init__.py | 4 +- robyn/mcp.py | 7 +- robyn/router.py | 9 +- robyn/ws.py | 3 +- src/executors/mod.rs | 4 +- src/types/request.rs | 2 +- src/types/response.rs | 26 +- 19 files changed, 341 insertions(+), 372 deletions(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 916b4a960..24b0fc59c 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -17,9 +17,9 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.9" + python-version: "3.10" - name: Install uv uses: astral-sh/setup-uv@v6 diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 9b3048274..531ea277a 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -16,7 +16,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.11" diff --git a/.github/workflows/preview-deployments.yml b/.github/workflows/preview-deployments.yml index a277c440a..661ed153d 100644 --- a/.github/workflows/preview-deployments.yml +++ b/.github/workflows/preview-deployments.yml @@ -16,15 +16,15 @@ env: jobs: macos: - runs-on: macos-13 + runs-on: macos-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - uses: dtolnay/rust-toolchain@stable @@ -34,15 +34,12 @@ jobs: uses: PyO3/maturin-action@v1 with: target: x86_64 - args: -i python --release --out dist --no-sdist - - name: Install build wheel - x86_64 - run: | - uv pip install --force-reinstall dist/robyn*.whl - cd ~ && python -c 'import robyn' + args: -i python --release --out dist - name: Build wheels - universal2 uses: PyO3/maturin-action@v1 with: - args: -i python --release --universal2 --out dist --no-sdist + target: universal2-apple-darwin + args: -i python --release --out dist - name: Install build wheel - universal2 run: | uv pip install --force-reinstall dist/robyn*_universal2.whl @@ -52,13 +49,13 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x64, x86] steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.target }} @@ -67,7 +64,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: -i python --release --out dist --no-sdist + args: -i python --release --out dist - name: Install build wheel shell: bash run: | @@ -78,14 +75,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x86_64, i686] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Build Wheels @@ -93,7 +90,7 @@ jobs: with: target: ${{ matrix.target }} manylinux: auto - args: -i python${{ matrix.python-version }} --release --out dist --no-sdist + args: -i python${{ matrix.python-version }} --release --out dist - name: Install build wheel if: matrix.target == 'x86_64' run: | @@ -107,11 +104,11 @@ jobs: matrix: python: [ - { version: "3.9", abi: "cp39-cp39" }, { version: "3.10", abi: "cp310-cp310" }, { version: "3.11", abi: "cp311-cp311" }, { version: "3.12", abi: "cp312-cp312" }, { version: "3.13", abi: "cp313-cp313" }, + { version: "3.14", abi: "cp314-cp314" }, ] target: [aarch64, armv7] steps: @@ -123,30 +120,26 @@ jobs: with: target: ${{ matrix.target }} manylinux: auto - args: -i python${{matrix.python.version}} --release --out dist --no-sdist + args: -i python${{matrix.python.version}} --release --out dist - uses: uraimo/run-on-arch-action@v2 name: Install build wheel with: arch: ${{ matrix.target }} - distro: ubuntu20.04 + distro: ubuntu22.04 githubToken: ${{ github.token }} # Mount the dist directory as /artifacts in the container dockerRunArgs: | --volume "${PWD}/dist:/artifacts" install: | apt update -y - apt install -y gcc musl-dev python3-dev # this is needed for psutil - apt install -y --no-install-recommends software-properties-common - add-apt-repository ppa:deadsnakes/ppa + apt install -y software-properties-common + add-apt-repository -y ppa:deadsnakes/ppa apt update -y - PYTHON=python${{ matrix.python.version }} - apt install -y $PYTHON $PYTHON-venv + apt install -y gcc musl-dev python3-dev python${{ matrix.python.version }} python${{ matrix.python.version }}-venv run: | ls -lrth /artifacts - PYTHON=python${{ matrix.python.version }} - $PYTHON --version - $PYTHON -m venv venv + python${{ matrix.python.version }} -m venv venv source venv/bin/activate - pip install --upgrade pip setuptools wheel - pip install --force-reinstall dist/robyn*.whl + python -m pip install --upgrade pip setuptools wheel + python -m pip install --force-reinstall /artifacts/robyn*.whl cd ~ && python -c 'import robyn' diff --git a/.github/workflows/python-CI.yml b/.github/workflows/python-CI.yml index 78f84bba3..30f3745cc 100644 --- a/.github/workflows/python-CI.yml +++ b/.github/workflows/python-CI.yml @@ -10,13 +10,13 @@ jobs: fail-fast: false matrix: os: ["windows", "ubuntu", "macos"] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] name: ${{ matrix.os }} tests with python ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Set up Nox diff --git a/.github/workflows/release-CI.yml b/.github/workflows/release-CI.yml index 2940d648f..d26f1c7cb 100644 --- a/.github/workflows/release-CI.yml +++ b/.github/workflows/release-CI.yml @@ -16,15 +16,15 @@ env: UV_SYSTEM_PYTHON: 1 jobs: macos: - runs-on: macos-13 + runs-on: macos-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - uses: dtolnay/rust-toolchain@stable @@ -35,14 +35,11 @@ jobs: with: target: x86_64 args: -i python --release --out dist - - name: Install build wheel - x86_64 - run: | - uv pip install --force-reinstall dist/robyn*.whl - cd ~ && python -c 'import robyn' - name: Build wheels - universal2 uses: PyO3/maturin-action@v1 with: - args: -i python --release --universal2 --out dist + target: universal2-apple-darwin + args: -i python --release --out dist - name: Install build wheel - universal2 run: | uv pip install --force-reinstall dist/robyn*_universal2.whl @@ -56,13 +53,13 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x64, x86] steps: - uses: actions/checkout@v3 - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.target }} @@ -86,14 +83,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x86_64, i686] steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Build Wheels @@ -119,11 +116,11 @@ jobs: matrix: python: [ - { version: "3.9", abi: "cp39-cp39" }, { version: "3.10", abi: "cp310-cp310" }, { version: "3.11", abi: "cp311-cp311" }, { version: "3.12", abi: "cp312-cp312" }, { version: "3.13", abi: "cp313-cp313" }, + { version: "3.14", abi: "cp314-cp314" }, ] target: [aarch64, armv7] steps: @@ -195,7 +192,7 @@ jobs: name: wheels - name: Install uv uses: astral-sh/setup-uv@v3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Publish to PyPi diff --git a/Cargo.lock b/Cargo.lock index 839753c12..8c72e668b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,11 +1290,10 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" dependencies = [ - "cfg-if", "indoc", "libc", "memoffset", @@ -1308,9 +1307,9 @@ dependencies = [ [[package]] name = "pyo3-async-runtimes" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0b83dc42f9d41f50d38180dad65f0c99763b65a3ff2a81bf351dd35a1df8bf" +checksum = "57ddb5b570751e93cc6777e81fee8087e59cd53b5043292f2a6d59d5bd80fdfd" dependencies = [ "futures", "once_cell", @@ -1321,9 +1320,9 @@ dependencies = [ [[package]] name = "pyo3-async-runtimes-macros" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf103ba4062fbb1e8022d9ed9b9830fbab074b2db0a0496c78e45a62f4330bcd" +checksum = "bcd7d70ee0ca1661c40407e6f84e4463ef2658c90a9e2fbbd4515b2bcdfcaeca" dependencies = [ "proc-macro2", "quote", @@ -1332,19 +1331,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" dependencies = [ "libc", "pyo3-build-config", @@ -1352,9 +1350,9 @@ dependencies = [ [[package]] name = "pyo3-log" -version = "0.12.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45192e5e4a4d2505587e27806c7b710c231c40c56f3bfc19535d0bb25df52264" +checksum = "2f8bae9ad5ba08b0b0ed2bb9c2bdbaeccc69cafca96d78cf0fbcea0d45d122bb" dependencies = [ "arc-swap", "log", @@ -1363,9 +1361,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1375,9 +1373,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" dependencies = [ "heck", "proc-macro2", @@ -1388,9 +1386,9 @@ dependencies = [ [[package]] name = "pythonize" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bcac0d0b71821f0d69e42654f1e15e5c94b85196446c4de9588951a2117e7b" +checksum = "a3a8f29db331e28c332c63496cfcbb822aca3d7320bc08b655d7fd0c29c50ede" dependencies = [ "pyo3", "serde", diff --git a/Cargo.toml b/Cargo.toml index 460893541..f2c6902bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ name = "robyn" crate-type = ["cdylib", "rlib"] [dependencies] -pyo3 = { version = "0.24.2", features = ["extension-module", "py-clone"]} -pyo3-async-runtimes = { version = "0.24", features = ["tokio-runtime"] } -pyo3-async-runtimes-macros = { version = "0.24" } -pyo3-log = "0.12.3" +pyo3 = { version = "0.27.2", features = ["extension-module", "py-clone"]} +pyo3-async-runtimes = { version = "0.27", features = ["tokio-runtime"] } +pyo3-async-runtimes-macros = { version = "0.27" } +pyo3-log = "0.13.2" tokio = { version = "1.40", features = ["full"] } dashmap = "5.4.3" anyhow = "1.0.69" @@ -32,7 +32,7 @@ matchit = "0.7.3" socket2 = { version = "0.5.1", features = ["all"] } uuid = { version = "1.3.0", features = ["serde", "v4"] } log = "0.4.17" -pythonize = "0.24" +pythonize = "0.27" serde = "1.0.187" serde_json = "1.0.109" once_cell = "1.8.0" diff --git a/README.md b/README.md index eccb7517a..6491a6769 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Downloads](https://static.pepy.tech/personalized-badge/Robyn?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Downloads)](https://pepy.tech/project/Robyn) [![GitHub tag](https://img.shields.io/github/tag/sparckles/Robyn?include_prereleases=&sort=semver&color=black)](https://github.com/sparckles/Robyn/releases/) [![License](https://img.shields.io/badge/License-BSD_2.0-black)](https://github.com/sparckles/Robyn/blob/main/LICENSE) -![Python](https://img.shields.io/badge/Support-Version%20%E2%89%A5%203.9-brightgreen) +![Python](https://img.shields.io/badge/Support-Version%20%E2%89%A5%203.10-brightgreen) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/sparckles/Robyn) [![view - Documentation](https://img.shields.io/badge/view-Documentation-blue?style=for-the-badge)](https://robyn.tech/documentation) @@ -99,11 +99,11 @@ $ python3 app.py --open-browser You can add more routes to your API. Check out the routes in [this file](https://github.com/sparckles/Robyn/blob/main/integration_tests/base_routes.py) as examples. -## 🐍 Python Version Support +### 🐍 Python Version Support Robyn is compatible with the following Python versions: -> Python >= 3.9 +> Python >= 3.10 It is recommended to use the latest version of Python for the best performances. @@ -152,7 +152,7 @@ If you still need help to get started, feel free to reach out on our [community #### Prerequisites Before starting, ensure you have the following installed: -- Python >= 3.9, <= 3.13 +- Python >= 3.10, <= 3.14 - Rust (latest stable) - C compiler (gcc/clang) diff --git a/integration_tests/test_dependency_injection.py b/integration_tests/test_dependency_injection.py index 198e763c2..13c65b65d 100644 --- a/integration_tests/test_dependency_injection.py +++ b/integration_tests/test_dependency_injection.py @@ -4,28 +4,28 @@ @pytest.mark.benchmark -def test_global_dependency_injection(benchmark): +def test_global_dependency_injection(): r = get("/sync/global_di") assert r.status_code == 200 assert r.text == "GLOBAL DEPENDENCY" @pytest.mark.benchmark -def test_router_dependency_injection(benchmark): +def test_router_dependency_injection(): r = get("/sync/router_di") assert r.status_code == 200 assert r.text == "ROUTER DEPENDENCY" @pytest.mark.benchmark -def test_subrouter_global_dependency_injection(benchmark): +def test_subrouter_global_dependency_injection(): r = get("/di_subrouter/subrouter_global_di") assert r.status_code == 200 assert r.text == "GLOBAL DEPENDENCY" @pytest.mark.benchmark -def test_subrouter_router_dependency_injection(benchmark): +def test_subrouter_router_dependency_injection(): r = get("/di_subrouter/subrouter_router_di") assert r.status_code == 200 assert r.text == "ROUTER DEPENDENCY" diff --git a/noxfile.py b/noxfile.py index 528d0736e..040619d24 100644 --- a/noxfile.py +++ b/noxfile.py @@ -3,7 +3,7 @@ import nox -@nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13"]) +@nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"]) def tests(session): session.run("pip", "install", "poetry==1.3.0") session.run("pip", "install", "maturin") diff --git a/poetry.lock b/poetry.lock index 9c77d6c53..541401eaf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "argcomplete" @@ -16,26 +16,6 @@ files = [ lint = ["flake8", "mypy"] test = ["coverage", "flake8", "mypy", "pexpect", "wheel"] -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - [[package]] name = "black" version = "23.1.0" @@ -78,7 +58,6 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -323,7 +302,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["test"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -382,31 +361,6 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["test"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.1.0" @@ -572,6 +526,36 @@ files = [ ] markers = {main = "extra == \"templating\""} +[[package]] +name = "maturin" +version = "1.7.4" +description = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "maturin-1.7.4-py3-none-linux_armv6l.whl", hash = "sha256:eb7b7753b733ae302c08f80bca7b0c3fda1eea665c2b1922c58795f35a54c833"}, + {file = "maturin-1.7.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0182a9638399c8835afd39d2aeacf56908e37cba3f7abb15816b9df6774fab81"}, + {file = "maturin-1.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:41a29c5b23f3ebdfe7633637e3de256579a1b2700c04cd68c16ed46934440c5a"}, + {file = "maturin-1.7.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:23fae44e345a2da5cb391ae878726fb793394826e2f97febe41710bd4099460e"}, + {file = "maturin-1.7.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:8b441521c151f0dbe70ed06fb1feb29b855d787bda038ff4330ca962e5d56641"}, + {file = "maturin-1.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:7ccb66d0c5297cf06652c5f72cb398f447d3a332eccf5d1e73b3fe14dbc9498c"}, + {file = "maturin-1.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:71f668f19e719048605dbca6a1f4d0dc03b987c922ad9c4bf5be03b9b278e4c3"}, + {file = "maturin-1.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:c179fcb2b494f19186781b667320e43d95b3e71fcb1c98fffad9ef6bd6e276b3"}, + {file = "maturin-1.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd5b4b95286f2f376437340f8a4908f4761587212170263084455be8099099a7"}, + {file = "maturin-1.7.4-py3-none-win32.whl", hash = "sha256:35487a424467d1fda4567cbb02d21f09febb10eda22f5fd647b130bc0767dc61"}, + {file = "maturin-1.7.4-py3-none-win_amd64.whl", hash = "sha256:f70c1c8ec9bd4749a53c0f3ae8fdbb326ce45be4f1c5551985ee25a6d7150328"}, + {file = "maturin-1.7.4-py3-none-win_arm64.whl", hash = "sha256:f3d38a6d0c7fd7b04bec30dd470b2173cf9bd184ab6220c1acaf49df6b48faf5"}, + {file = "maturin-1.7.4.tar.gz", hash = "sha256:2b349d742a07527d236f0b4b6cab26f53ebecad0ceabfc09ec4c6a396e3176f9"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +patchelf = ["patchelf"] +zig = ["ziglang (>=0.10.0,<0.13.0)"] + [[package]] name = "mdurl" version = "0.1.2" @@ -658,84 +642,99 @@ tox-to-nox = ["jinja2", "tox (<4)"] [[package]] name = "orjson" -version = "3.11.0" +version = "3.11.5" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "orjson-3.11.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b8913baba9751f7400f8fa4ec18a8b618ff01177490842e39e47b66c1b04bc79"}, - {file = "orjson-3.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d4d86910554de5c9c87bc560b3bdd315cc3988adbdc2acf5dda3797079407ed"}, - {file = "orjson-3.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84ae3d329360cf18fb61b67c505c00dedb61b0ee23abfd50f377a58e7d7bed06"}, - {file = "orjson-3.11.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47a54e660414baacd71ebf41a69bb17ea25abb3c5b69ce9e13e43be7ac20e342"}, - {file = "orjson-3.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2560b740604751854be146169c1de7e7ee1e6120b00c1788ec3f3a012c6a243f"}, - {file = "orjson-3.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7f9cd995da9e46fbac0a371f0ff6e89a21d8ecb7a8a113c0acb147b0a32f73"}, - {file = "orjson-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf728cb3a013bdf9f4132575404bf885aa773d8bb4205656575e1890fc91990"}, - {file = "orjson-3.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c27de273320294121200440cd5002b6aeb922d3cb9dab3357087c69f04ca6934"}, - {file = "orjson-3.11.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4430ec6ff1a1f4595dd7e0fad991bdb2fed65401ed294984c490ffa025926325"}, - {file = "orjson-3.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:325be41a8d7c227d460a9795a181511ba0e731cf3fee088c63eb47e706ea7559"}, - {file = "orjson-3.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9760217b84d1aee393b4436fbe9c639e963ec7bc0f2c074581ce5fb3777e466"}, - {file = "orjson-3.11.0-cp310-cp310-win32.whl", hash = "sha256:fe36e5012f886ff91c68b87a499c227fa220e9668cea96335219874c8be5fab5"}, - {file = "orjson-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebeecd5d5511b3ca9dc4e7db0ab95266afd41baf424cc2fad8c2d3a3cdae650a"}, - {file = "orjson-3.11.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1785df7ada75c18411ff7e20ac822af904a40161ea9dfe8c55b3f6b66939add6"}, - {file = "orjson-3.11.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a57899bebbcea146616a2426d20b51b3562b4bc9f8039a3bd14fae361c23053d"}, - {file = "orjson-3.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fbc2fc825aff1456dd358c11a0ad7912a4cb4537d3db92e5334af7463a967"}, - {file = "orjson-3.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4305a638f4cf9bed3746ca3b7c242f14e05177d5baec2527026e0f9ee6c24fb7"}, - {file = "orjson-3.11.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1235fe7bbc37164f69302199d46f29cfb874018738714dccc5a5a44042c79c77"}, - {file = "orjson-3.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a640e3954e7b4fcb160097551e54cafbde9966be3991932155b71071077881aa"}, - {file = "orjson-3.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d750b97d22d5566955e50b02c622f3a1d32744d7a578c878b29a873190ccb7a"}, - {file = "orjson-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfcfe498484161e011f8190a400591c52b026de96b3b3cbd3f21e8999b9dc0e"}, - {file = "orjson-3.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:feaed3ed43a1d2df75c039798eb5ec92c350c7d86be53369bafc4f3700ce7df2"}, - {file = "orjson-3.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1120607ec8fc98acf8c54aac6fb0b7b003ba883401fa2d261833111e2fa071"}, - {file = "orjson-3.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c4b48d9775b0cf1f0aca734f4c6b272cbfacfac38e6a455e6520662f9434afb7"}, - {file = "orjson-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f018ed1986d79434ac712ff19f951cd00b4dfcb767444410fbb834ebec160abf"}, - {file = "orjson-3.11.0-cp311-cp311-win32.whl", hash = "sha256:08e191f8a55ac2c00be48e98a5d10dca004cbe8abe73392c55951bfda60fc123"}, - {file = "orjson-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:b5a4214ea59c8a3b56f8d484b28114af74e9fba0956f9be5c3ce388ae143bf1f"}, - {file = "orjson-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:57e8e7198a679ab21241ab3f355a7990c7447559e35940595e628c107ef23736"}, - {file = "orjson-3.11.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b4089f940c638bb1947d54e46c1cd58f4259072fcc97bc833ea9c78903150ac9"}, - {file = "orjson-3.11.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:8335a0ba1c26359fb5c82d643b4c1abbee2bc62875e0f2b5bde6c8e9e25eb68c"}, - {file = "orjson-3.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c1c9772dafc811d16d6a7efa3369a739da15d1720d6e58ebe7562f54d6f4a2"}, - {file = "orjson-3.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9457ccbd8b241fb4ba516417a4c5b95ba0059df4ac801309bcb4ec3870f45ad9"}, - {file = "orjson-3.11.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0846e13abe79daece94a00b92574f294acad1d362be766c04245b9b4dd0e47e1"}, - {file = "orjson-3.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5587c85ae02f608a3f377b6af9eb04829606f518257cbffa8f5081c1aacf2e2f"}, - {file = "orjson-3.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7a1964a71c1567b4570c932a0084ac24ad52c8cf6253d1881400936565ed438"}, - {file = "orjson-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5a8243e73690cc6e9151c9e1dd046a8f21778d775f7d478fa1eb4daa4897c61"}, - {file = "orjson-3.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51646f6d995df37b6e1b628f092f41c0feccf1d47e3452c6e95e2474b547d842"}, - {file = "orjson-3.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:2fb8ca8f0b4e31b8aaec674c7540649b64ef02809410506a44dc68d31bd5647b"}, - {file = "orjson-3.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:64a6a3e94a44856c3f6557e6aa56a6686544fed9816ae0afa8df9077f5759791"}, - {file = "orjson-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69f95d484938d8fab5963e09131bcf9fbbb81fa4ec132e316eb2fb9adb8ce78"}, - {file = "orjson-3.11.0-cp312-cp312-win32.whl", hash = "sha256:8514f9f9c667ce7d7ef709ab1a73e7fcab78c297270e90b1963df7126d2b0e23"}, - {file = "orjson-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:41b38a894520b8cb5344a35ffafdf6ae8042f56d16771b2c5eb107798cee85ee"}, - {file = "orjson-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:5579acd235dd134467340b2f8a670c1c36023b5a69c6a3174c4792af7502bd92"}, - {file = "orjson-3.11.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a8ba9698655e16746fdf5266939427da0f9553305152aeb1a1cc14974a19cfb"}, - {file = "orjson-3.11.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:67133847f9a35a5ef5acfa3325d4a2f7fe05c11f1505c4117bb086fc06f2a58f"}, - {file = "orjson-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f797d57814975b78f5f5423acb003db6f9be5186b72d48bd97a1000e89d331d"}, - {file = "orjson-3.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:28acd19822987c5163b9e03a6e60853a52acfee384af2b394d11cb413b889246"}, - {file = "orjson-3.11.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8d38d9e1e2cf9729658e35956cf01e13e89148beb4cb9e794c9c10c5cb252f8"}, - {file = "orjson-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05f094edd2b782650b0761fd78858d9254de1c1286f5af43145b3d08cdacfd51"}, - {file = "orjson-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d09176a4a9e04a5394a4a0edd758f645d53d903b306d02f2691b97d5c736a9e"}, - {file = "orjson-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a585042104e90a61eda2564d11317b6a304eb4e71cd33e839f5af6be56c34d3"}, - {file = "orjson-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2218629dbfdeeb5c9e0573d59f809d42f9d49ae6464d2f479e667aee14c3ef4"}, - {file = "orjson-3.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:613e54a2b10b51b656305c11235a9c4a5c5491ef5c283f86483d4e9e123ed5e4"}, - {file = "orjson-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9dac7fbf3b8b05965986c5cfae051eb9a30fced7f15f1d13a5adc608436eb486"}, - {file = "orjson-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b64b254414e2be55ac5257124b5602c5f0b4d06b80bd27d1165efe8f36e836"}, - {file = "orjson-3.11.0-cp313-cp313-win32.whl", hash = "sha256:359cbe11bc940c64cb3848cf22000d2aef36aff7bfd09ca2c0b9cb309c387132"}, - {file = "orjson-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:0759b36428067dc777b202dd286fbdd33d7f261c6455c4238ea4e8474358b1e6"}, - {file = "orjson-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:51cdca2f36e923126d0734efaf72ddbb5d6da01dbd20eab898bdc50de80d7b5a"}, - {file = "orjson-3.11.0-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d79c180cfb3ae68f13245d0ff551dca03d96258aa560830bf8a223bd68d8272c"}, - {file = "orjson-3.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:105bca887532dc71ce4b05a5de95dea447a310409d7a8cf0cb1c4a120469e9ad"}, - {file = "orjson-3.11.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acf5a63ae9cdb88274126af85913ceae554d8fd71122effa24a53227abbeee16"}, - {file = "orjson-3.11.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:894635df36c0be32f1c8c8607e853b8865edb58e7618e57892e85d06418723eb"}, - {file = "orjson-3.11.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02dd4f0a1a2be943a104ce5f3ec092631ee3e9f0b4bb9eeee3400430bd94ddef"}, - {file = "orjson-3.11.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:720b4bb5e1b971960a62c2fa254c2d2a14e7eb791e350d05df8583025aa59d15"}, - {file = "orjson-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf058105a8aed144e0d1cfe7ac4174748c3fc7203f225abaeac7f4121abccb0"}, - {file = "orjson-3.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a2788f741e5a0e885e5eaf1d91d0c9106e03cb9575b0c55ba36fd3d48b0b1e9b"}, - {file = "orjson-3.11.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c60c99fe1e15894367b0340b2ff16c7c69f9c3f3a54aa3961a58c102b292ad94"}, - {file = "orjson-3.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:99d17aab984f4d029b8f3c307e6be3c63d9ee5ef55e30d761caf05e883009949"}, - {file = "orjson-3.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e98f02e23611763c9e5dfcb83bd33219231091589f0d1691e721aea9c52bf329"}, - {file = "orjson-3.11.0-cp39-cp39-win32.whl", hash = "sha256:923301f33ea866b18f8836cf41d9c6d33e3b5cab8577d20fed34ec29f0e13a0d"}, - {file = "orjson-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:475491bb78af2a0170f49e90013f1a0f1286527f3617491f8940d7e5da862da7"}, - {file = "orjson-3.11.0.tar.gz", hash = "sha256:2e4c129da624f291bcc607016a99e7f04a353f6874f3bd8d9b47b88597d5f700"}, + {file = "orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1"}, + {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870"}, + {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09"}, + {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd"}, + {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac"}, + {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e"}, + {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f"}, + {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18"}, + {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a"}, + {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7"}, + {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401"}, + {file = "orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8"}, + {file = "orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167"}, + {file = "orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8"}, + {file = "orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc"}, + {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968"}, + {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7"}, + {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd"}, + {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9"}, + {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef"}, + {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9"}, + {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125"}, + {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814"}, + {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5"}, + {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880"}, + {file = "orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d"}, + {file = "orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1"}, + {file = "orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c"}, + {file = "orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d"}, + {file = "orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626"}, + {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f"}, + {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85"}, + {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9"}, + {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626"}, + {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa"}, + {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477"}, + {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e"}, + {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69"}, + {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3"}, + {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca"}, + {file = "orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98"}, + {file = "orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875"}, + {file = "orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe"}, + {file = "orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629"}, + {file = "orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3"}, + {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39"}, + {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f"}, + {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51"}, + {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8"}, + {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706"}, + {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f"}, + {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863"}, + {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228"}, + {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2"}, + {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05"}, + {file = "orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef"}, + {file = "orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583"}, + {file = "orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287"}, + {file = "orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0"}, + {file = "orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81"}, + {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f"}, + {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e"}, + {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7"}, + {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb"}, + {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4"}, + {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad"}, + {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829"}, + {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac"}, + {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d"}, + {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439"}, + {file = "orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499"}, + {file = "orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310"}, + {file = "orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5"}, + {file = "orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9"}, + {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec"}, + {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00"}, + {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71"}, + {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c"}, + {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5"}, + {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb"}, + {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56"}, + {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111"}, + {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8"}, + {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a"}, + {file = "orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1"}, + {file = "orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30"}, + {file = "orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5"}, ] [[package]] @@ -873,50 +872,56 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "7.2.1" +version = "9.0.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, ] [package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-codspeed" -version = "3.0.0" +version = "3.2.0" description = "Pytest plugin to create CodSpeed benchmarks" optional = false python-versions = ">=3.9" groups = ["test"] files = [ - {file = "pytest_codspeed-3.0.0-py3-none-any.whl", hash = "sha256:ab1b8cb9da72e0d394718333d1abc7bea38524e09fd4854bc70a91abbcdcb20e"}, - {file = "pytest_codspeed-3.0.0.tar.gz", hash = "sha256:c5b80100ea32dd44079bb2db298288763eb8fe859eafa1650a8711bd2c32fd06"}, + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"}, + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:109f9f4dd1088019c3b3f887d003b7d65f98a7736ca1d457884f5aa293e8e81c"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2f69a03b52c9bb041aec1b8ee54b7b6c37a6d0a948786effa4c71157765b6da"}, + {file = "pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39"}, + {file = "pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155"}, ] [package.dependencies] cffi = ">=1.17.1" -filelock = ">=3.12.2" -importlib-metadata = {version = ">=8.5.0", markers = "python_version < \"3.10\""} pytest = ">=3.8" rich = ">=13.8.1" -setuptools = {version = "*", markers = "python_full_version >= \"3.12.0\""} [package.extras] -build = ["semver (>=3.0.2)"] compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"] test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] @@ -1046,29 +1051,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.1.3" -description = "An extremely fast Python linter, written in Rust." +version = "0.8.5" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.1.3-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b46d43d51f7061652eeadb426a9e3caa1e0002470229ab2fc19de8a7b0766901"}, - {file = "ruff-0.1.3-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b8afeb9abd26b4029c72adc9921b8363374f4e7edb78385ffaa80278313a15f9"}, - {file = "ruff-0.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca3cf365bf32e9ba7e6db3f48a4d3e2c446cd19ebee04f05338bc3910114528b"}, - {file = "ruff-0.1.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4874c165f96c14a00590dcc727a04dca0cfd110334c24b039458c06cf78a672e"}, - {file = "ruff-0.1.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eec2dd31eed114e48ea42dbffc443e9b7221976554a504767ceaee3dd38edeb8"}, - {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dc3ec4edb3b73f21b4aa51337e16674c752f1d76a4a543af56d7d04e97769613"}, - {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e3de9ed2e39160800281848ff4670e1698037ca039bda7b9274f849258d26ce"}, - {file = "ruff-0.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c595193881922cc0556a90f3af99b1c5681f0c552e7a2a189956141d8666fe8"}, - {file = "ruff-0.1.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f75e670d529aa2288cd00fc0e9b9287603d95e1536d7a7e0cafe00f75e0dd9d"}, - {file = "ruff-0.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76dd49f6cd945d82d9d4a9a6622c54a994689d8d7b22fa1322983389b4892e20"}, - {file = "ruff-0.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:918b454bc4f8874a616f0d725590277c42949431ceb303950e87fef7a7d94cb3"}, - {file = "ruff-0.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8859605e729cd5e53aa38275568dbbdb4fe882d2ea2714c5453b678dca83784"}, - {file = "ruff-0.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0b6c55f5ef8d9dd05b230bb6ab80bc4381ecb60ae56db0330f660ea240cb0d4a"}, - {file = "ruff-0.1.3-py3-none-win32.whl", hash = "sha256:3e7afcbdcfbe3399c34e0f6370c30f6e529193c731b885316c5a09c9e4317eef"}, - {file = "ruff-0.1.3-py3-none-win_amd64.whl", hash = "sha256:7a18df6638cec4a5bd75350639b2bb2a2366e01222825562c7346674bdceb7ea"}, - {file = "ruff-0.1.3-py3-none-win_arm64.whl", hash = "sha256:12fd53696c83a194a2db7f9a46337ce06445fb9aa7d25ea6f293cf75b21aca9f"}, - {file = "ruff-0.1.3.tar.gz", hash = "sha256:3ba6145369a151401d5db79f0a47d50e470384d0d89d0d6f7fab0b589ad07c34"}, + {file = "ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88"}, + {file = "ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7"}, + {file = "ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb"}, + {file = "ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50"}, + {file = "ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4"}, + {file = "ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd"}, + {file = "ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc"}, + {file = "ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b"}, + {file = "ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd"}, + {file = "ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb"}, + {file = "ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725"}, + {file = "ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea"}, + {file = "ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8"}, + {file = "ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5"}, + {file = "ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed"}, + {file = "ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47"}, + {file = "ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb"}, + {file = "ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317"}, ] [[package]] @@ -1086,28 +1092,6 @@ files = [ [package.dependencies] toml = ">=0.10.2" -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["test"] -markers = "python_full_version >= \"3.12.0\"" -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - [[package]] name = "termcolor" version = "2.5.0" @@ -1142,7 +1126,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "test"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1201,7 +1185,7 @@ files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] -markers = {test = "python_version < \"3.11\""} +markers = {test = "python_version == \"3.10\""} [[package]] name = "urllib3" @@ -1222,56 +1206,68 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvloop" -version = "0.21.0" +version = "0.22.1" description = "Fast implementation of asyncio event loop on top of libuv" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.8.1" groups = ["main"] markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation == \"CPython\" and platform_machine != \"armv7l\"" files = [ - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, - {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c"}, + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7"}, + {file = "uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f"}, ] [package.extras] dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=25.3.0,<25.4.0)", "pycodestyle (>=2.11.0,<2.12.0)"] [[package]] name = "virtualenv" @@ -1368,31 +1364,10 @@ docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["test"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] templating = ["jinja2"] [metadata] lock-version = "2.1" -python-versions = "^3.9" -content-hash = "d7653d607537a233dfde48f2851b8a03ebe485da509cf1a93b7c69df086a2a3d" +python-versions = "^3.10" +content-hash = "f8cab2b8efc19345f249b95d1cd67e5976001e7bffc8a2148b905d700fa118ed" diff --git a/pyproject.toml b/pyproject.toml index c2860e2ea..0100aa85d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=0.12,<0.13"] +requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [project] @@ -16,20 +16,20 @@ classifiers = [ "Operating System :: OS Independent", "Topic :: Internet :: WWW/HTTP", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", ] dependencies = [ "inquirerpy == 0.3.4", "multiprocess == 0.70.14", - "orjson == 3.11.0", + "orjson >= 3.11.5, < 4.0.0", "rustimport == 1.3.4", # conditional - "uvloop~=0.21.0; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'", + "uvloop~=0.22.1; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'", "watchdog == 4.0.1", ] @@ -51,14 +51,14 @@ dev = [ "black==23.1", "commitizen==2.40", "isort==5.11.5", - "maturin==0.14.12", + "maturin==1.7.4", "pre-commit==2.21.0", - "ruff==0.1.3", + "ruff>=0.9.0", ] test = [ "nox==2023.4.22", - "pytest==7.2.1", - "pytest-codspeed==1.2.2", + "pytest>=9.0.2", + "pytest-codspeed>=4.2.0", "requests==2.28.2", "websocket-client==1.5.0", ] @@ -72,15 +72,15 @@ authors = ["Sanskar Jethi "] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" inquirerpy = "0.3.4" -maturin = "0.14.12" +maturin = "1.7.4" watchdog = "4.0.1" multiprocess = "0.70.14" -uvloop = { version = "0.21.0", markers = "sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')" } +uvloop = { version = "0.22.1", markers = "sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')" } jinja2 = { version = "3.0.1", optional = true } rustimport = "^1.3.4" -orjson = "^3.11.0" +orjson = "^3.11.5" [tool.poetry.extras] templating = ["jinja2"] @@ -89,7 +89,7 @@ templating = ["jinja2"] optional = true [tool.poetry.group.dev.dependencies] -ruff = "0.1.3" +ruff = ">=0.9.0" black = "23.1" isort = "5.11.5" pre-commit = "2.21.0" @@ -99,8 +99,8 @@ commitizen = "2.40" optional = true [tool.poetry.group.test.dependencies] -pytest = "7.2.1" -pytest-codspeed = "3.0.0" +pytest = "^9.0.2" +pytest-codspeed = "^4.2.0" requests = "2.28.2" nox = "2023.4.22" websocket-client = "1.5.0" @@ -112,7 +112,7 @@ test_server = { callable = "integration_tests.base_routes:main" } line-length = 160 exclude = ["src/*", ".git", "docs"] -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 10 [tool.isort] @@ -121,7 +121,7 @@ line_length = 160 [tool.black] line-length = 160 -target-version = ['py38'] +target-version = ['py39'] include = '\.pyi?$' extend-exclude = ''' /( @@ -137,5 +137,10 @@ extend-exclude = ''' )/ ''' +[tool.pytest.ini_options] +markers = [ + "benchmark: marks tests as benchmarks for performance measurement (deselect with '-m \"not benchmark\"')", +] + [tool.maturin] module-name = "robyn" diff --git a/robyn/__init__.py b/robyn/__init__.py index b58cc5901..78ce102dc 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -1,4 +1,4 @@ -import asyncio +import inspect import logging import os import socket @@ -284,7 +284,7 @@ def _add_event_handler(self, event_type: Events, handler: Callable) -> None: if event_type not in {Events.STARTUP, Events.SHUTDOWN}: return - is_async = asyncio.iscoroutinefunction(handler) + is_async = inspect.iscoroutinefunction(handler) self.event_handlers[event_type] = FunctionInfo(handler, is_async, 0, {}, {}) def startup_handler(self, handler: Callable) -> None: diff --git a/robyn/mcp.py b/robyn/mcp.py index 9d3b63002..c71d2e788 100644 --- a/robyn/mcp.py +++ b/robyn/mcp.py @@ -6,7 +6,6 @@ to MCP clients like Claude Desktop or other AI applications. """ -import asyncio import inspect import json import logging @@ -251,7 +250,7 @@ async def _handle_read_resource(self, params: Dict[str, Any]) -> Dict[str, Any]: sig = inspect.signature(handler) handler_params = list(sig.parameters.keys()) - if asyncio.iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): if uri_params: # Use URI parameters for templated resources content = await handler(**uri_params) @@ -299,7 +298,7 @@ async def _handle_call_tool(self, params: Dict[str, Any]) -> Dict[str, Any]: handler = self.tools[name] # Call the tool handler - if asyncio.iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): result = await handler(**arguments) else: result = handler(**arguments) @@ -324,7 +323,7 @@ async def _handle_get_prompt(self, params: Dict[str, Any]) -> Dict[str, Any]: handler = self.prompts[name] # Call the prompt handler - if asyncio.iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): result = await handler(**arguments) else: result = handler(**arguments) diff --git a/robyn/router.py b/robyn/router.py index 55dd2c201..7df5196da 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -1,7 +1,6 @@ import inspect import logging from abc import ABC, abstractmethod -from asyncio import iscoroutinefunction from functools import wraps from types import CoroutineType from typing import Callable, Dict, List, NamedTuple, Optional, Union @@ -231,7 +230,7 @@ def inner_handler(*args, **kwargs): else: _logger.debug(f"Dependency {dependency} is not used in the handler {handler.__name__}") - if iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): function = FunctionInfo( async_inner_handler, True, @@ -295,7 +294,7 @@ def add_route( # type: ignore function = FunctionInfo( handler, - iscoroutinefunction(handler), + inspect.iscoroutinefunction(handler), len(params), params, new_injected_dependencies, @@ -350,7 +349,7 @@ def inner_handler(*args, **kwargs): return handler(*args, **kwargs) if endpoint is not None: - if iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): self.add_route( middleware_type, endpoint, @@ -363,7 +362,7 @@ def inner_handler(*args, **kwargs): else: params = dict(inspect.signature(handler).parameters) - if iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): self.global_middlewares.append( GlobalMiddleware( middleware_type, diff --git a/robyn/ws.py b/robyn/ws.py index 2c1b56f11..a1b39d5fc 100644 --- a/robyn/ws.py +++ b/robyn/ws.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import inspect from typing import TYPE_CHECKING, Callable @@ -34,7 +33,7 @@ def inner(handler): params = dict(inspect.signature(handler).parameters) num_params = len(params) - is_async = asyncio.iscoroutinefunction(handler) + is_async = inspect.iscoroutinefunction(handler) injected_dependencies = self.dependencies.get_dependency_map(self) diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 9caecc363..532d70c56 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -30,7 +30,7 @@ where { let handler = function.handler.bind(py).downcast()?; let kwargs = function.kwargs.bind(py); - let function_args: PyObject = function_args + let function_args: Py = function_args .clone() .into_pyobject(py) .map_err(|e| { @@ -70,7 +70,7 @@ pub async fn execute_middleware_function( function: &FunctionInfo, ) -> Result where - T: Clone + for<'a> FromPyObject<'a> + for<'py> IntoPyObject<'py>, + T: Clone + for<'a, 'py> FromPyObject<'a, 'py> + for<'py> IntoPyObject<'py>, for<'py> >::Error: std::fmt::Debug, { if function.is_async { diff --git a/src/types/request.rs b/src/types/request.rs index 15b61cc3b..494d4c0d1 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -263,7 +263,7 @@ impl PyRequest { Ok(()) } - pub fn json(&self, py: Python) -> PyResult { + pub fn json(&self, py: Python) -> PyResult> { match self.body.downcast_bound::(py) { Ok(python_string) => match serde_json::from_str(python_string.extract()?) { Ok(Value::Object(map)) => { diff --git a/src/types/response.rs b/src/types/response.rs index a439aa889..c85670e0c 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -29,7 +29,7 @@ pub struct Response { pub struct StreamingResponse { pub status_code: u16, pub headers: Headers, - pub content_generator: PyObject, + pub content_generator: Py, } #[derive(Debug)] @@ -71,7 +71,7 @@ impl Responder for Response { } impl StreamingResponse { - pub fn new(status_code: u16, headers: Headers, content_generator: PyObject) -> Self { + pub fn new(status_code: u16, headers: Headers, content_generator: Py) -> Self { Self { status_code, headers, @@ -107,7 +107,7 @@ impl Responder for StreamingResponse { } fn create_python_stream( - generator: PyObject, + generator: Py, ) -> Pin> + Send>> { Box::pin(futures::stream::unfold(generator, |generator| async move { // Use spawn_blocking to execute the Python generator call in a separate thread @@ -257,7 +257,7 @@ pub struct PyStreamingResponse { #[pyo3(get, set)] pub headers: Py, #[pyo3(get)] - pub content: PyObject, + pub content: Py, #[pyo3(get)] pub media_type: String, } @@ -267,7 +267,7 @@ impl PyStreamingResponse { #[new] pub fn new( py: Python, - content: PyObject, + content: Py, status_code: Option, headers: Option>, media_type: Option, @@ -379,8 +379,10 @@ impl PyResponse { } } -impl FromPyObject<'_> for Response { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Response { + type Error = PyErr; + + fn extract(obj: pyo3::Borrowed<'_, '_, PyAny>) -> Result { // Only extract from actual Response objects, not StreamingResponse let type_name = obj.get_type().name()?; debug!("Attempting to extract Response from type: {}", type_name); @@ -412,8 +414,10 @@ impl FromPyObject<'_> for Response { } } -impl FromPyObject<'_> for StreamingResponse { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for StreamingResponse { + type Error = PyErr; + + fn extract(obj: pyo3::Borrowed<'_, '_, PyAny>) -> Result { // Check if it's a StreamingResponse by checking attributes rather than strict type name let type_name = obj .get_type() @@ -472,7 +476,7 @@ impl FromPyObject<'_> for StreamingResponse { } Err(e) => { debug!("Failed to extract headers: {}", e); - return Err(e); + return Err(e.into()); } }, Err(e) => { @@ -525,7 +529,7 @@ impl FromPyObject<'_> for StreamingResponse { } } - let content: PyObject = match obj.getattr("content") { + let content: pyo3::Py = match obj.getattr("content") { Ok(attr) => { debug!("Successfully got content attribute"); attr.unbind() From c80c9a2e78c3761400c770e5cba2ff312d5a5783 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Tue, 13 Jan 2026 22:33:58 +0000 Subject: [PATCH 013/106] Release 0.74.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c72e668b..b62ea39be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.73.0" +version = "0.74.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index f2c6902bf..90447aec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.73.0" +version = "0.74.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index 0100aa85d..6f634fe19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.73.0" +version = "0.74.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] license = { file = "LICENSE" } @@ -66,7 +66,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.73.0" +version = "0.74.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From c73e32da594c2bf1ea91247e9498b60d9a717520 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:37:25 +0000 Subject: [PATCH 014/106] chore: feat pypi release description (#1281) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6f634fe19..0f6a3564c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ build-backend = "maturin" name = "robyn" version = "0.74.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." +readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] license = { file = "LICENSE" } classifiers = [ From 3d14cd4d9488abb9cef98ce15597752358b3687c Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 18 Jan 2026 00:35:45 +0000 Subject: [PATCH 015/106] feat: make request object available in after request (#1283) * feat: make request object available in after request * update --- .../en/api_reference/middlewares.mdx | 23 +++- .../zh/api_reference/middlewares.mdx | 23 +++- src/executors/mod.rs | 113 ++++++++++++++++++ src/server.rs | 46 +++---- 4 files changed, 172 insertions(+), 33 deletions(-) diff --git a/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx b/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx index 97a431f45..efd09fce5 100644 --- a/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx @@ -76,7 +76,7 @@ Batman was excited to learn that he could add events as functions as well as dec A before request middleware is a function that executes before each request. It can modify the request object or perform any other operation before the request is processed. An after request middleware is a function that executes after each request. It can modify the response object or perform any other operation after the request is processed. - Every before request middleware should accept a request object and return a request object. Every after request middleware should accept a response object and return a response object on happy case scenario. + Every before request middleware should accept a request object and return a request object. Every after-request middleware should accept a response object and return a response object on happy case scenario. After-request middlewares can also optionally accept the request object as the first parameter to access request data. The execution of the before request middleware is stopped if any of the before request middleware returns a response object. The response object is returned to the client without executing the after request middleware or the main entry point code. @@ -96,21 +96,32 @@ Batman was excited to learn that he could add events as functions as well as dec @app.after_request("/") def hello_after_request(response: Response): - response.headers.set("after", "sync_after_request"") + response.headers.set("after", "sync_after_request") return response ``` ```python {{ title: 'typed' }} - from request import Request, Response + from robyn import Request, Response @app.before_request("/") - async def hello_before_request(request): + async def hello_before_request(request: Request): request.headers.set("before", "sync_before_request") return request @app.after_request("/") - def hello_after_request(response): - response.headers.set("after", "sync_after_request"") + def hello_after_request(response: Response): + response.headers.set("after", "sync_after_request") + return response + ``` + + ```python {{ title: 'after_request with request access' }} + from robyn import Request, Response + + @app.after_request("/") + def hello_after_request(request: Request, response: Response): + # Access request data in after_request + response.headers.set("request_path", request.url.path) + response.headers.set("after", "sync_after_request") return response ``` diff --git a/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx b/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx index a25f257f3..7df7ea8d2 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx @@ -78,7 +78,7 @@ export const description = 该函数在每个请求处理之前执行,可以修改请求对象或执行其他操作。 请求后中间件:该函数在每个请求处理之后执行,可以修改响应对象或执行其他操作。 -每个请求前中间件都应接收并返回一个请求对象;每个请求后中间件都应接收并返回一个响应对象。 +每个请求前中间件都应接收并返回一个请求对象;每个请求后中间件都应接收并返回一个响应对象。请求后中间件也可以选择性地接收请求对象作为第一个参数,以便访问请求数据。 如果某个请求前中间件返回了响应对象,执行将被中止,响应对象直接返回给客户端,后续的请求后中间件和主处理函数将不再执行。 @@ -95,21 +95,32 @@ export const description = @app.after_request("/") def hello_after_request(response: Response): - response.headers.set("after", "sync_after_request"") + response.headers.set("after", "sync_after_request") return response ``` ```python {{ title: 'typed' }} - from request import Request, Response + from robyn import Request, Response @app.before_request("/") - async def hello_before_request(request): + async def hello_before_request(request: Request): request.headers.set("before", "sync_before_request") return request @app.after_request("/") - def hello_after_request(response): - response.headers.set("after", "sync_after_request"") + def hello_after_request(response: Response): + response.headers.set("after", "sync_after_request") + return response + ``` + + ```python {{ title: '在 after_request 中访问请求对象' }} + from robyn import Request, Response + + @app.after_request("/") + def hello_after_request(request: Request, response: Response): + # 在 after_request 中访问请求数据 + response.headers.set("request_path", request.url.path) + response.headers.set("after", "sync_after_request") return response ``` diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 532d70c56..7a5204224 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -105,6 +105,119 @@ where } } +// Execute the after_request middleware function with both request and response +// This allows after_request callbacks to access the request object +#[inline] +fn get_function_output_with_two_args<'a, T, U>( + function: &'a FunctionInfo, + py: Python<'a>, + first_arg: &T, + second_arg: &U, +) -> Result, PyErr> +where + T: Clone + for<'py> IntoPyObject<'py>, + U: Clone + for<'py> IntoPyObject<'py>, + for<'py> >::Error: std::fmt::Debug, + for<'py> >::Error: std::fmt::Debug, +{ + let handler = function.handler.bind(py).downcast()?; + let kwargs = function.kwargs.bind(py); + let first_arg: Py = first_arg + .clone() + .into_pyobject(py) + .map_err(|e| { + PyErr::new::(format!( + "Failed to convert first arg: {:?}", + e + )) + })? + .into_any() + .unbind(); + let second_arg: Py = second_arg + .clone() + .into_pyobject(py) + .map_err(|e| { + PyErr::new::(format!( + "Failed to convert second arg: {:?}", + e + )) + })? + .into_any() + .unbind(); + debug!("Function args: {:?}, {:?}", first_arg, second_arg); + + match function.number_of_params { + 0 => handler.call0(), + 1 => { + // If function only has 1 parameter, pass only the response (second_arg) for backward compatibility + if pyo3::types::PyDictMethods::get_item(kwargs, "global_dependencies") + .is_ok_and(|it| !it.is_none()) + || pyo3::types::PyDictMethods::get_item(kwargs, "router_dependencies") + .is_ok_and(|it| !it.is_none()) + { + handler.call((), Some(kwargs)) + } else { + handler.call1((second_arg,)) + } + } + 2 => { + // If function has 2 parameters, pass both request and response + // Check if there are dependencies to pass via kwargs + if pyo3::types::PyDictMethods::get_item(kwargs, "global_dependencies") + .is_ok_and(|it| !it.is_none()) + || pyo3::types::PyDictMethods::get_item(kwargs, "router_dependencies") + .is_ok_and(|it| !it.is_none()) + { + handler.call((first_arg, second_arg), Some(kwargs)) + } else { + handler.call1((first_arg, second_arg)) + } + } + _ => handler.call((first_arg, second_arg), Some(kwargs)), + } +} + +// Execute the after_request middleware function with both request and response +#[inline] +pub async fn execute_after_middleware_function( + request: &Request, + response: &Response, + function: &FunctionInfo, +) -> Result { + if function.is_async { + let output: Py = Python::with_gil(|py| { + pyo3_async_runtimes::tokio::into_future(get_function_output_with_two_args( + function, py, request, response, + )?) + })? + .await?; + + Python::with_gil(|py| -> Result { + // Try response extraction first, then request + match output.extract::(py) { + Ok(response) => Ok(MiddlewareReturn::Response(response)), + Err(_) => match output.extract::(py) { + Ok(request) => Ok(MiddlewareReturn::Request(request)), + Err(e) => Err(e.into()), + }, + } + }) + } else { + Python::with_gil(|py| -> Result { + let output = get_function_output_with_two_args(function, py, request, response)?; + debug!("After middleware output: {:?}", output); + + match output.extract::() { + Ok(response) => Ok(MiddlewareReturn::Response(response)), + Err(_) => match output.extract::() { + Ok(request) => Ok(MiddlewareReturn::Request(request)), + Err(e) => Err(e.into()), + }, + } + }) + } +} + #[inline] pub async fn execute_http_function( request: &Request, diff --git a/src/server.rs b/src/server.rs index f0a7ecee7..f0b61c1f7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,6 @@ use crate::executors::{ - execute_http_function, execute_middleware_function, execute_startup_handler, + execute_after_middleware_function, execute_http_function, execute_middleware_function, + execute_startup_handler, }; use crate::routers::const_router::ConstRouter; @@ -571,26 +572,29 @@ async fn index( for after_middleware in after_middlewares { // Middleware only works with standard responses if let ResponseType::Standard(std_response) = response { - response = match execute_middleware_function(&std_response, &after_middleware).await { - Ok(MiddlewareReturn::Request(_)) => { - error!("After middleware returned a request"); - return ResponseType::Standard(Response::internal_server_error(None)); - } - Ok(MiddlewareReturn::Response(r)) => { - debug!("Response returned: {:?}", r); - ResponseType::Standard(r) - } - Err(e) => { - error!( - "Error while executing after middleware function for endpoint `{}`: {}", - req.uri().path(), - get_traceback(e.downcast_ref::().unwrap()) - ); - return ResponseType::Standard(Response::internal_server_error(Some( - &std_response.headers, - ))); - } - }; + response = + match execute_after_middleware_function(&request, &std_response, &after_middleware) + .await + { + Ok(MiddlewareReturn::Request(_)) => { + error!("After middleware returned a request"); + return ResponseType::Standard(Response::internal_server_error(None)); + } + Ok(MiddlewareReturn::Response(r)) => { + debug!("Response returned: {:?}", r); + ResponseType::Standard(r) + } + Err(e) => { + error!( + "Error while executing after middleware function for endpoint `{}`: {}", + req.uri().path(), + get_traceback(e.downcast_ref::().unwrap()) + ); + return ResponseType::Standard(Response::internal_server_error(Some( + &std_response.headers, + ))); + } + }; } else { // Skip middleware for streaming responses debug!("Skipping after middleware for streaming response"); From e5240114942450944d0d18c6ffc681a61fe89f4d Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 18 Jan 2026 03:14:08 +0000 Subject: [PATCH 016/106] fix: preserve JSON types in request parsing (#1284) * fix: json parsing --- integration_tests/base_routes.py | 27 ++++ .../helpers/http_methods_helpers.py | 24 ++++ integration_tests/test_json_types.py | 119 ++++++++++++++++++ src/types/request.rs | 57 ++++++++- 4 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 integration_tests/test_json_types.py diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 128f1a057..4a15097d8 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -604,6 +604,33 @@ async def request_json(request: Request): return json["key"] +# JSON type preservation test +@app.post("/sync/request_json/types") +def sync_json_types(request: Request): + """Returns the JSON data with Python type names for verification""" + data = request.json() + result = {} + for key, value in data.items(): + result[key] = { + "value": value, + "type": type(value).__name__, + } + return result + + +@app.post("/async/request_json/types") +async def async_json_types(request: Request): + """Returns the JSON data with Python type names for verification""" + data = request.json() + result = {} + for key, value in data.items(): + result[key] = { + "value": value, + "type": type(value).__name__, + } + return result + + # --- PUT --- # dict diff --git a/integration_tests/helpers/http_methods_helpers.py b/integration_tests/helpers/http_methods_helpers.py index 10c9367f9..2c0ba70c9 100644 --- a/integration_tests/helpers/http_methods_helpers.py +++ b/integration_tests/helpers/http_methods_helpers.py @@ -61,6 +61,30 @@ def post( return response +def json_post( + endpoint: str, + json_data: Optional[dict] = None, + expected_status_code: int = 200, + headers: dict = {}, + should_check_response: bool = True, +) -> requests.Response: + """ + Makes a POST request with JSON body to the given endpoint and checks the response. + + endpoint str: The endpoint to make the request to. + json_data Optional[dict]: The JSON data to send with the request. + expected_status_code int: The expected status code of the response. + headers dict: The headers to send with the request. + should_check_response bool: A boolean to indicate if the status code and headers should be checked. + """ + + endpoint = endpoint.strip("/") + response = requests.post(f"{BASE_URL}/{endpoint}", json=json_data, headers=headers) + if should_check_response: + check_response(response, expected_status_code) + return response + + def multipart_post( endpoint: str, files: Optional[dict] = None, diff --git a/integration_tests/test_json_types.py b/integration_tests/test_json_types.py new file mode 100644 index 000000000..ab8d5b9b7 --- /dev/null +++ b/integration_tests/test_json_types.py @@ -0,0 +1,119 @@ +import pytest + +from integration_tests.helpers.http_methods_helpers import json_post + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_integer_type_preserved(function_type: str, session): + """Test that integer values in JSON are preserved as integers, not strings""" + json_data = {"lid": 570, "count": 42} + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + assert result["lid"]["value"] == 570 + assert result["lid"]["type"] == "int" + assert result["count"]["value"] == 42 + assert result["count"]["type"] == "int" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_null_type_preserved(function_type: str, session): + """Test that null values in JSON are preserved as None, not string 'null'""" + json_data = {"start": None, "end": None} + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + assert result["start"]["value"] is None + assert result["start"]["type"] == "NoneType" + assert result["end"]["value"] is None + assert result["end"]["type"] == "NoneType" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_boolean_type_preserved(function_type: str, session): + """Test that boolean values in JSON are preserved as booleans, not strings""" + json_data = {"active": True, "deleted": False} + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + assert result["active"]["value"] is True + assert result["active"]["type"] == "bool" + assert result["deleted"]["value"] is False + assert result["deleted"]["type"] == "bool" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_float_type_preserved(function_type: str, session): + """Test that float values in JSON are preserved as floats""" + json_data = {"price": 19.99, "rate": 0.15} + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + assert result["price"]["value"] == 19.99 + assert result["price"]["type"] == "float" + assert result["rate"]["value"] == 0.15 + assert result["rate"]["type"] == "float" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_string_type_preserved(function_type: str, session): + """Test that string values in JSON remain strings""" + json_data = {"field_name": "mobile", "field_value": "111000111"} + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + assert result["field_name"]["value"] == "mobile" + assert result["field_name"]["type"] == "str" + assert result["field_value"]["value"] == "111000111" + assert result["field_value"]["type"] == "str" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_array_type_preserved(function_type: str, session): + """Test that array values in JSON are preserved as lists""" + json_data = {"items": [1, 2, 3], "tags": ["a", "b"]} + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + assert result["items"]["value"] == [1, 2, 3] + assert result["items"]["type"] == "list" + assert result["tags"]["value"] == ["a", "b"] + assert result["tags"]["type"] == "list" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_nested_object_type_preserved(function_type: str, session): + """Test that nested object values in JSON are preserved as dicts""" + json_data = {"user": {"name": "John", "age": 30}} + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + assert result["user"]["value"] == {"name": "John", "age": 30} + assert result["user"]["type"] == "dict" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_mixed_types_preserved(function_type: str, session): + """Test the exact scenario from the bug report - mixed types in one request""" + json_data = { + "lid": 570, + "start": None, + "field_name": "mobile", + "field_value": "111000111", + } + res = json_post(f"/{function_type}/request_json/types", json_data=json_data) + result = res.json() + + # Integer should remain integer (not become "570") + assert result["lid"]["value"] == 570 + assert result["lid"]["type"] == "int" + + # None should remain None (not become "null") + assert result["start"]["value"] is None + assert result["start"]["type"] == "NoneType" + + # Strings should remain strings + assert result["field_name"]["value"] == "mobile" + assert result["field_name"]["type"] == "str" + assert result["field_value"]["value"] == "111000111" + assert result["field_value"]["type"] == "str" diff --git a/src/types/request.rs b/src/types/request.rs index 494d4c0d1..92b7bb114 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -5,7 +5,7 @@ use actix_web::{ }; use futures_util::StreamExt as _; use log::debug; -use pyo3::types::{PyBytes, PyDict, PyString}; +use pyo3::types::{PyBytes, PyDict, PyList, PyString}; use pyo3::{exceptions::PyValueError, prelude::*, IntoPyObject}; use serde_json::Value; use std::collections::HashMap; @@ -271,11 +271,7 @@ impl PyRequest { for (key, value) in map.iter() { let py_key = key.to_string().into_pyobject(py)?.into_any(); - let py_value = match value { - Value::String(s) => s.as_str().into_pyobject(py)?.into_any(), - _ => value.to_string().into_pyobject(py)?.into_any(), - }; - + let py_value = json_value_to_py(py, value)?; dict.set_item(py_key, py_value)?; } @@ -287,3 +283,52 @@ impl PyRequest { } } } + +/// Maximum allowed recursion depth for JSON parsing to prevent stack overflow attacks. +const MAX_JSON_DEPTH: usize = 128; + +/// Converts a serde_json::Value to a Python object with proper type preservation. +/// This is a convenience wrapper that starts recursion with MAX_JSON_DEPTH. +fn json_value_to_py(py: Python, value: &Value) -> PyResult> { + json_value_to_py_with_depth(py, value, MAX_JSON_DEPTH) +} + +/// Converts a serde_json::Value to a Python object with recursion depth limiting. +fn json_value_to_py_with_depth(py: Python, value: &Value, depth: usize) -> PyResult> { + if depth == 0 { + return Err(PyValueError::new_err( + "JSON nesting depth exceeds maximum allowed limit", + )); + } + + match value { + Value::Null => Ok(py.None()), + Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any().unbind()), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(i.into_pyobject(py)?.into_any().unbind()) + } else if let Some(u) = n.as_u64() { + Ok(u.into_pyobject(py)?.into_any().unbind()) + } else if let Some(f) = n.as_f64() { + Ok(f.into_pyobject(py)?.into_any().unbind()) + } else { + Err(PyValueError::new_err("Invalid number in JSON")) + } + } + Value::String(s) => Ok(s.as_str().into_pyobject(py)?.into_any().unbind()), + Value::Array(arr) => { + let list = PyList::empty(py); + for item in arr { + list.append(json_value_to_py_with_depth(py, item, depth - 1)?)?; + } + Ok(list.into_pyobject(py)?.into_any().unbind()) + } + Value::Object(map) => { + let dict = PyDict::new(py); + for (k, v) in map { + dict.set_item(k, json_value_to_py_with_depth(py, v, depth - 1)?)?; + } + Ok(dict.into_pyobject(py)?.into_any().unbind()) + } + } +} From 7a6d224aaaeb3137e68b08ed09823a07217e1445 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 18 Jan 2026 03:26:15 +0000 Subject: [PATCH 017/106] chore: fix typing docstrings (#1285) --- robyn/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/robyn/__init__.py b/robyn/__init__.py index 78ce102dc..0580eada4 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -520,11 +520,15 @@ def inner(handler): return inner - def include_router(self, router): + def include_router(self, router: "SubRouter"): """ - The method to include the routes from another router + The method to include the routes from another router. + Merge another SubRouter's routes, middlewares, websocket routes, and dependencies into this router. + Note: This operation mutates the current router's internal collections (route list, middleware lists, + websocket routes, and dependencies) and does not deep-copy the included router. Callers should ensure + there are no path or name conflicts before including a router. - :param router Robyn: the router object to include the routes from + :param router SubRouter: the router object to include the routes from """ self.included_routers.append(router) From 55b1088fd83b5eb9f5a24572d15e30c1c33591a8 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 18 Jan 2026 03:46:03 +0000 Subject: [PATCH 018/106] feat: allow empty returns in websockets (#1280) * Allow empty returns in WebSocket handlers Fixes #1263 This change allows WebSocket event handlers (connect, message, close) to operate without requiring a return statement. Previously, handlers had to return a string or the program would exit with an error. Changes: - Updated async WebSocket executor to extract Option instead of &str - Only send message if handler returns Some(string), otherwise continue silently - Sync handlers already supported optional returns, now async handlers do too - Added test cases demonstrating empty returns on all event types - Updated existing handlers to use optional returns instead of empty strings --- CHANGELOG.md | 4 ++ .../en/api_reference/websockets.mdx | 69 +++++++++++++++++++ .../zh/api_reference/websockets.mdx | 69 +++++++++++++++++++ integration_tests/base_routes.py | 28 +++++++- integration_tests/test_web_sockets.py | 15 ++++ src/executors/web_socket_executors.rs | 8 ++- 6 files changed, 189 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a6944c2..16407a54c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ [Full Changelog](https://github.com/sparckles/robyn/compare/v0.26.1...HEAD) +**Implemented enhancements:** + +- Allow empty returns on websocket handling [\#1263](https://github.com/sparckles/robyn/issues/1263) + **Closed issues:** - Payload reached size limit. [\#463](https://github.com/sparckles/robyn/issues/463) diff --git a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx index 346b4129e..a712e904c 100644 --- a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx @@ -60,6 +60,75 @@ To handle real-time bidirectional communication, Batman learned how to work with + +--- + +## Optional Return Values {{ tag: 'Optional Returns', label: 'Optional Returns' }} + + + + Batman discovered that WebSocket handlers don't always need to return a value. Sometimes, he just wanted to process a message or perform an action without sending a response back to the client. + + "Not every message needs a reply," Batman realized. "Sometimes I just need to log data or trigger an action." + + WebSocket handlers (`connect`, `message`, and `close`) can optionally return a string. If no value is returned (or `None` is returned), no message will be sent to the client. + + + + + ```python {{ title: 'untyped' }} + from robyn import Robyn, WebSocket + + app = Robyn(__file__) + websocket = WebSocket(app, "/web_socket") + + @websocket.on("connect") + async def connect(): + # No return needed - just log the connection + print("Client connected") + + @websocket.on("message") + def message(ws, msg): + # Process message without responding + process_analytics(msg) + # No return statement needed + + @websocket.on("close") + async def close(): + # Explicitly return None - no message sent + cleanup_resources() + return None + ``` + + ```python {{title: 'typed'}} + from robyn import Robyn, WebSocket, WebSocketConnector + + app = Robyn(__file__) + websocket = WebSocket(app, "/web_socket") + + @websocket.on("connect") + async def connect() -> None: + # No return needed - just log the connection + print("Client connected") + + @websocket.on("message") + def message(ws: WebSocketConnector, msg: str) -> None: + # Process message without responding + process_analytics(msg) + # No return statement needed + + @websocket.on("close") + async def close() -> None: + # Explicitly return None - no message sent + cleanup_resources() + return None + ``` + + + + +--- + diff --git a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx index 3524409f8..fdea7aace 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx @@ -60,6 +60,75 @@ export const description = + +--- + +## 可选返回值 {{ tag: '可选返回值', label: '可选返回值' }} + + + + 蝙蝠侠发现 WebSocket 处理程序并不总是需要返回值。有时,他只是想处理消息或执行某个操作,而不需要向客户端发送响应。 + + "并非每条消息都需要回复,"蝙蝠侠意识到,"有时我只需要记录数据或触发某个操作。" + + WebSocket 处理程序(`connect`、`message` 和 `close`)可以选择性地返回字符串。如果不返回值(或返回 `None`),则不会向客户端发送任何消息。 + + + + + ```python {{ title: 'untyped' }} + from robyn import Robyn, WebSocket + + app = Robyn(__file__) + websocket = WebSocket(app, "/web_socket") + + @websocket.on("connect") + async def connect(): + # 无需返回 - 仅记录连接 + print("客户端已连接") + + @websocket.on("message") + def message(ws, msg): + # 处理消息但不响应 + process_analytics(msg) + # 无需返回语句 + + @websocket.on("close") + async def close(): + # 显式返回 None - 不发送消息 + cleanup_resources() + return None + ``` + + ```python {{title: 'typed'}} + from robyn import Robyn, WebSocket, WebSocketConnector + + app = Robyn(__file__) + websocket = WebSocket(app, "/web_socket") + + @websocket.on("connect") + async def connect() -> None: + # 无需返回 - 仅记录连接 + print("客户端已连接") + + @websocket.on("message") + def message(ws: WebSocketConnector, msg: str) -> None: + # 处理消息但不响应 + process_analytics(msg) + # 无需返回语句 + + @websocket.on("close") + async def close() -> None: + # 显式返回 None - 不发送消息 + cleanup_resources() + return None + ``` + + + + +--- + diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 4a15097d8..becffa93a 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -25,6 +25,8 @@ websocket_di.inject_global(GLOBAL_DEPENDENCY="GLOBAL DEPENDENCY") websocket_di.inject(ROUTER_DEPENDENCY="ROUTER DEPENDENCY") +websocket_empty_returns = WebSocket(app, "/web_socket_empty_returns") + current_file_path = pathlib.Path(__file__).parent.resolve() jinja_template = JinjaTemplate(os.path.join(current_file_path, "templates")) @@ -102,12 +104,34 @@ async def di_message_connect(global_dependencies, router_dependencies): @websocket_di.on("message") async def di_message(): - return "" + # Test empty return - should not send anything + pass @websocket_di.on("close") async def di_message_close(): - return "" + # Test empty return - should not send anything + pass + + +@websocket_empty_returns.on("connect") +async def empty_connect(): + """Test async handler with no return""" + # No return statement - should not send anything + pass + + +@websocket_empty_returns.on("message") +def empty_message_sync(): + """Test sync handler with no return""" + # No return statement - should not send anything + pass + + +@websocket_empty_returns.on("close") +async def empty_close(): + """Test async handler with explicit None return""" + return None # ===== Lifecycle handlers ===== diff --git a/integration_tests/test_web_sockets.py b/integration_tests/test_web_sockets.py index da3d3ee9d..0c47eef0f 100644 --- a/integration_tests/test_web_sockets.py +++ b/integration_tests/test_web_sockets.py @@ -66,3 +66,18 @@ def test_websocket_di(session): ws = create_connection(f"{BASE_URL}/web_socket_di") assert ws.recv() == msg + + +def test_websocket_empty_returns(session): + """Test that WebSocket handlers can return nothing without causing errors""" + ws = create_connection(f"{BASE_URL}/web_socket_empty_returns") + + # Connect handler returns None - no message should be received on connection + # We need to send a message to verify the connection is still active + ws.send("test message") + + # Message handler returns None - no response should be sent + # The socket should still be open, not crashed + # We can verify this by closing the connection gracefully + ws.close() + # If we got here without exceptions, the test passed diff --git a/src/executors/web_socket_executors.rs b/src/executors/web_socket_executors.rs index 91f0edea1..83cf95bd9 100644 --- a/src/executors/web_socket_executors.rs +++ b/src/executors/web_socket_executors.rs @@ -86,10 +86,14 @@ pub fn execute_ws_function( }); let f = async { let output = fut.await.unwrap(); - Python::with_gil(|py| output.extract::<&str>(py).unwrap().to_string()) + Python::with_gil(|py| output.extract::>(py).unwrap()) } .into_actor(ws) - .map(|res, _, ctx| ctx.text(res)); + .map(|res, _, ctx| { + if let Some(msg) = res { + ctx.text(msg); + } + }); ctx.spawn(f); } else { Python::with_gil(|py| { From 118204fa2b283189f448116351b2ae75c70314dd Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:04:08 +0000 Subject: [PATCH 019/106] fix: a build issue in the docs website (#1289) --- docs_src/mdx/rehype.mjs | 8 ++++++-- .../pages/documentation/en/api_reference/websockets.mdx | 2 -- .../pages/documentation/zh/api_reference/websockets.mdx | 2 -- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs_src/mdx/rehype.mjs b/docs_src/mdx/rehype.mjs index 070c88f81..94679b266 100644 --- a/docs_src/mdx/rehype.mjs +++ b/docs_src/mdx/rehype.mjs @@ -125,8 +125,12 @@ export const rehypePlugins = [ transform: (article) => { article.children.splice(0, 1) let heading = article.children.find((n) => n.tagName === 'h2') - article.properties = { ...heading.properties, title: toString(heading) } - heading.properties = {} + if (heading) { + article.properties = { ...heading.properties, title: toString(heading) } + heading.properties = {} + } else { + article.properties = {} + } return article }, }, diff --git a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx index a712e904c..4f52101b0 100644 --- a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx @@ -127,8 +127,6 @@ To handle real-time bidirectional communication, Batman learned how to work with ---- - diff --git a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx index fdea7aace..c96fe128f 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx @@ -127,8 +127,6 @@ export const description = ---- - From 7f24505d103b299ce3d2d25773b4f33251605af1 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Mon, 19 Jan 2026 23:30:06 +0000 Subject: [PATCH 020/106] Release 0.75.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- poetry.lock | 105 ++++++++++++++++++------------------------------- pyproject.toml | 4 +- 4 files changed, 43 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b62ea39be..f33ca5a87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.74.0" +version = "0.75.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 90447aec0..c9e11bb19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.74.0" +version = "0.75.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/poetry.lock b/poetry.lock index 541401eaf..5d4c2d232 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "argcomplete" @@ -526,36 +526,6 @@ files = [ ] markers = {main = "extra == \"templating\""} -[[package]] -name = "maturin" -version = "1.7.4" -description = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "maturin-1.7.4-py3-none-linux_armv6l.whl", hash = "sha256:eb7b7753b733ae302c08f80bca7b0c3fda1eea665c2b1922c58795f35a54c833"}, - {file = "maturin-1.7.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0182a9638399c8835afd39d2aeacf56908e37cba3f7abb15816b9df6774fab81"}, - {file = "maturin-1.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:41a29c5b23f3ebdfe7633637e3de256579a1b2700c04cd68c16ed46934440c5a"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:23fae44e345a2da5cb391ae878726fb793394826e2f97febe41710bd4099460e"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:8b441521c151f0dbe70ed06fb1feb29b855d787bda038ff4330ca962e5d56641"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:7ccb66d0c5297cf06652c5f72cb398f447d3a332eccf5d1e73b3fe14dbc9498c"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:71f668f19e719048605dbca6a1f4d0dc03b987c922ad9c4bf5be03b9b278e4c3"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:c179fcb2b494f19186781b667320e43d95b3e71fcb1c98fffad9ef6bd6e276b3"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd5b4b95286f2f376437340f8a4908f4761587212170263084455be8099099a7"}, - {file = "maturin-1.7.4-py3-none-win32.whl", hash = "sha256:35487a424467d1fda4567cbb02d21f09febb10eda22f5fd647b130bc0767dc61"}, - {file = "maturin-1.7.4-py3-none-win_amd64.whl", hash = "sha256:f70c1c8ec9bd4749a53c0f3ae8fdbb326ce45be4f1c5551985ee25a6d7150328"}, - {file = "maturin-1.7.4-py3-none-win_arm64.whl", hash = "sha256:f3d38a6d0c7fd7b04bec30dd470b2173cf9bd184ab6220c1acaf49df6b48faf5"}, - {file = "maturin-1.7.4.tar.gz", hash = "sha256:2b349d742a07527d236f0b4b6cab26f53ebecad0ceabfc09ec4c6a396e3176f9"}, -] - -[package.dependencies] -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -patchelf = ["patchelf"] -zig = ["ziglang (>=0.10.0,<0.13.0)"] - [[package]] name = "mdurl" version = "0.1.2" @@ -896,24 +866,28 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests [[package]] name = "pytest-codspeed" -version = "3.2.0" +version = "4.2.0" description = "Pytest plugin to create CodSpeed benchmarks" optional = false python-versions = ">=3.9" groups = ["test"] files = [ - {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"}, - {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"}, - {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035"}, - {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58"}, - {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b"}, - {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2"}, - {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f"}, - {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b"}, - {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:109f9f4dd1088019c3b3f887d003b7d65f98a7736ca1d457884f5aa293e8e81c"}, - {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2f69a03b52c9bb041aec1b8ee54b7b6c37a6d0a948786effa4c71157765b6da"}, - {file = "pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39"}, - {file = "pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155"}, + {file = "pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609828b03972966b75b9b7416fa2570c4a0f6124f67e02d35cd3658e64312a7b"}, + {file = "pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23a0c0fbf8bb4de93a3454fd9e5efcdca164c778aaef0a9da4f233d85cb7f5b8"}, + {file = "pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2de87bde9fbc6fd53f0fd21dcf2599c89e0b8948d49f9bad224edce51c47e26b"}, + {file = "pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95aeb2479ca383f6b18e2cc9ebcd3b03ab184980a59a232aea6f370bbf59a1e3"}, + {file = "pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d4fefbd4ae401e2c60f6be920a0be50eef0c3e4a1f0a1c83962efd45be38b39"}, + {file = "pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309b4227f57fcbb9df21e889ea1ae191d0d1cd8b903b698fdb9ea0461dbf1dfe"}, + {file = "pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72aab8278452a6d020798b9e4f82780966adb00f80d27a25d1274272c54630d5"}, + {file = "pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:684fcd9491d810ded653a8d38de4835daa2d001645f4a23942862950664273f8"}, + {file = "pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50794dabea6ec90d4288904452051e2febace93e7edf4ca9f2bce8019dd8cd37"}, + {file = "pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0ebd87f2a99467a1cfd8e83492c4712976e43d353ee0b5f71cbb057f1393aca"}, + {file = "pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbbb2d61b85bef8fc7e2193f723f9ac2db388a48259d981bbce96319043e9830"}, + {file = "pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748411c832147bfc85f805af78a1ab1684f52d08e14aabe22932bbe46c079a5f"}, + {file = "pytest_codspeed-4.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:238e17abe8f08d8747fa6c7acff34fefd3c40f17a56a7847ca13dc8d6e8c6009"}, + {file = "pytest_codspeed-4.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0881a736285f33b9a8894da8fe8e1775aa1a4310226abe5d1f0329228efb680c"}, + {file = "pytest_codspeed-4.2.0-py3-none-any.whl", hash = "sha256:e81bbb45c130874ef99aca97929d72682733527a49f84239ba575b5cb843bab0"}, + {file = "pytest_codspeed-4.2.0.tar.gz", hash = "sha256:04b5d0bc5a1851ba1504d46bf9d7dbb355222a69f2cd440d54295db721b331f7"}, ] [package.dependencies] @@ -923,8 +897,6 @@ rich = ">=13.8.1" [package.extras] compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] -lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"] -test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] [[package]] name = "pyyaml" @@ -1051,30 +1023,31 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.8.5" +version = "0.14.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88"}, - {file = "ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7"}, - {file = "ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5"}, - {file = "ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed"}, - {file = "ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47"}, - {file = "ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb"}, - {file = "ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317"}, + {file = "ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b"}, + {file = "ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed"}, + {file = "ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841"}, + {file = "ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c"}, + {file = "ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b"}, + {file = "ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae"}, + {file = "ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e"}, + {file = "ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c"}, + {file = "ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680"}, + {file = "ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef"}, + {file = "ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247"}, + {file = "ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47"}, ] [[package]] @@ -1370,4 +1343,4 @@ templating = ["jinja2"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "f8cab2b8efc19345f249b95d1cd67e5976001e7bffc8a2148b905d700fa118ed" +content-hash = "0a1c19c18dbcd1915142f96e512b136c470446c98d1d6dd508312c5fb752a0f4" diff --git a/pyproject.toml b/pyproject.toml index 0f6a3564c..45651b505 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.74.0" +version = "0.75.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -67,7 +67,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.74.0" +version = "0.75.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 14fc4d8af43c1ee2d4d066fedb5209fece45856a Mon Sep 17 00:00:00 2001 From: Promger <96533520+thePromger@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:32:45 +0530 Subject: [PATCH 021/106] fix: Fixes routing issues which causes bugs like authentication bypass... (#1265) * fix: normalize endpoint paths in middleware and request handling * normalize endpoint when making request from actix http request * fix: some formatting & linting issues * update _normalize_endpoint to handle None and empty strings too * refactor: simplify route matching logic and update normalization rules * test: enhance authentication tests for endpoint consistency * using _normalized_endpoint in subRouter correct way, after doing changes in function * fix: update endpoint normalization to handle empty strings and slashes * Improved test cases, which checks request url after successful response, to confirm whether request goes to correct endpoint & internal routing works for trailing slashes * feat: enhance authentication tests, docs & and improve endpoint normalization * Added documentation for middleware. * Fixed formatting & linting issues. * Completed the TODO in line 127, by log warning if openAPI config file not found. * Created new tests funcs for trailing slash endpoints. * feat: refactoring endpoint normalizing code, to avoid logical bugs --- .../helpers/http_methods_helpers.py | 16 +++--- integration_tests/test_authentication.py | 43 ++++++++++++++ robyn/__init__.py | 57 +++++++++++++------ robyn/router.py | 16 ++++++ src/routers/http_router.rs | 30 +--------- src/server.rs | 17 +++--- src/types/request.rs | 13 ++++- 7 files changed, 128 insertions(+), 64 deletions(-) diff --git a/integration_tests/helpers/http_methods_helpers.py b/integration_tests/helpers/http_methods_helpers.py index 2c0ba70c9..9ad39f2e3 100644 --- a/integration_tests/helpers/http_methods_helpers.py +++ b/integration_tests/helpers/http_methods_helpers.py @@ -30,7 +30,7 @@ def get( headers dict: The headers to send with the request. should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") response = requests.get(f"{BASE_URL}/{endpoint}", headers=headers) if should_check_response: check_response(response, expected_status_code) @@ -54,7 +54,7 @@ def post( should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") response = requests.post(f"{BASE_URL}/{endpoint}", data=data, headers=headers) if should_check_response: check_response(response, expected_status_code) @@ -100,7 +100,7 @@ def multipart_post( should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") response = requests.post(f"{BASE_URL}/{endpoint}", files=files) if should_check_response: check_response(response, expected_status_code) @@ -123,7 +123,7 @@ def put( should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") response = requests.put(f"{BASE_URL}/{endpoint}", data=data, headers=headers) if should_check_response: check_response(response, expected_status_code) @@ -146,7 +146,7 @@ def patch( should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") response = requests.patch(f"{BASE_URL}/{endpoint}", data=data, headers=headers) if should_check_response: check_response(response, expected_status_code) @@ -169,7 +169,7 @@ def delete( should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") response = requests.delete(f"{BASE_URL}/{endpoint}", data=data, headers=headers) if should_check_response: check_response(response, expected_status_code) @@ -192,7 +192,7 @@ def head( should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") response = requests.head(f"{BASE_URL}/{endpoint}", data=data, headers=headers) if should_check_response: check_response(response, expected_status_code) @@ -218,7 +218,7 @@ def generic_http_helper( should_check_response bool: A boolean to indicate if the status code and headers should be checked. """ - endpoint = endpoint.strip("/") + endpoint = endpoint.lstrip("/") if method not in ["get", "post", "put", "patch", "delete", "options", "trace"]: raise ValueError(f"{method} method must be one of get, post, put, patch, delete") if method == "get": diff --git a/integration_tests/test_authentication.py b/integration_tests/test_authentication.py index 34a33f608..1d2cec79f 100644 --- a/integration_tests/test_authentication.py +++ b/integration_tests/test_authentication.py @@ -1,3 +1,5 @@ +from urllib.parse import urlparse + import pytest from integration_tests.helpers.http_methods_helpers import get @@ -10,6 +12,15 @@ def test_valid_authentication(session, function_type: str): assert r.text == "authenticated" +@pytest.mark.benchmark +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_valid_authentication_trailing_slash(session, function_type: str): + r = get(f"/{function_type}/auth/", headers={"Authorization": "Bearer valid"}) + # Checks whether request is being sent to exact /trailing/ route. + assert urlparse(r.url).path == f"/{function_type}/auth/" + assert r.text == "authenticated" + + @pytest.mark.benchmark @pytest.mark.parametrize("function_type", ["sync", "async"]) def test_invalid_authentication_token(session, function_type: str): @@ -22,6 +33,19 @@ def test_invalid_authentication_token(session, function_type: str): assert r.headers.get("WWW-Authenticate") == "BearerGetter" +@pytest.mark.benchmark +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_invalid_authentication_token_trailing_slash(session, function_type: str): + r = get( + f"/{function_type}/auth/", + headers={"Authorization": "Bearer invalid"}, + should_check_response=False, + ) + assert urlparse(r.url).path == f"/{function_type}/auth/" + assert r.status_code == 401 + assert r.headers.get("WWW-Authenticate") == "BearerGetter" + + @pytest.mark.benchmark @pytest.mark.parametrize("function_type", ["sync", "async"]) def test_invalid_authentication_header(session, function_type: str): @@ -34,9 +58,28 @@ def test_invalid_authentication_header(session, function_type: str): assert r.headers.get("WWW-Authenticate") == "BearerGetter" +@pytest.mark.benchmark +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_invalid_authentication_header_trailing_slash(session, function_type: str): + r = get( + f"/{function_type}/auth/", + headers={"Authorization": "Bear valid"}, + should_check_response=False, + ) + assert r.status_code == 401 + assert r.headers.get("WWW-Authenticate") == "BearerGetter" + + @pytest.mark.benchmark @pytest.mark.parametrize("function_type", ["sync", "async"]) def test_invalid_authentication_no_token(session, function_type: str): r = get(f"/{function_type}/auth", should_check_response=False) assert r.status_code == 401 assert r.headers.get("WWW-Authenticate") == "BearerGetter" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_invalid_authentication_no_token_trailing_slash(session, function_type: str): + r = get(f"/{function_type}/auth/", should_check_response=False) + assert r.status_code == 401 + assert r.headers.get("WWW-Authenticate") == "BearerGetter" diff --git a/robyn/__init__.py b/robyn/__init__.py index 0580eada4..d67284a60 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -29,7 +29,7 @@ __version__ = get_version() -def _normalize_endpoint(endpoint: str) -> str: +def _normalize_endpoint(endpoint: Optional[str], treat_empty_as_root: bool = False) -> Optional[str]: """ Normalize an endpoint to ensure consistent routing. @@ -37,22 +37,33 @@ def _normalize_endpoint(endpoint: str) -> str: - Root "/" remains unchanged - All other endpoints get leading slash added if missing - Trailing slashes are removed from all endpoints except root + - Empty or blank strings are handled based on treat_empty_as_root flag + - treat_empty_as_root is used for prefixes where empty/blank strings are valid Args: - endpoint: The endpoint path to normalize + endpoint: The endpoint path to normalize. + treat_empty_as_root (used for prefixes): + If True, empty/blank strings are converted to "/" (root). + If False, empty/blank strings return None (invalid endpoint). Returns: - Normalized endpoint path + Normalized endpoint path or None if invalid. """ - if endpoint == "/": + if endpoint is None or (not endpoint and not treat_empty_as_root): + return None + + # Remove trailing slashes + endpoint = endpoint.strip().rstrip("/") + + # Handle empty result + if not endpoint: return "/" # Add leading slash if missing if not endpoint.startswith("/"): endpoint = "/" + endpoint - # Remove trailing slash - return endpoint.rstrip("/") + return endpoint config = Config() @@ -120,7 +131,8 @@ def init_openapi(self, openapi_file_path: Optional[str]) -> None: self.openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path)) elif Path(self.directory_path).joinpath("openapi.json").exists(): self.openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json")) - # TODO! what about when the elif fails? + else: + logger.debug("No OpenAPI spec file found; using auto-generated documentation only.", color=Colors.YELLOW) def _handle_dev_mode(self): cli_dev_mode = self.config.dev # --dev @@ -172,12 +184,15 @@ def add_route( } route_type = http_methods[route_type] - if auth_required: - self.middleware_router.add_auth_middleware(endpoint, route_type)(handler) - # Normalize endpoint before adding normalized_endpoint = _normalize_endpoint(endpoint) + if normalized_endpoint is None: + raise ValueError("Endpoint cannot be blank, do specify '/' for root endpoint") + + if auth_required: + self.middleware_router.add_auth_middleware(normalized_endpoint, route_type)(handler) + # Check if this exact route (method + normalized_endpoint) already exists route_key = f"{route_type}:{normalized_endpoint}" if not hasattr(self, "_added_routes"): @@ -229,8 +244,7 @@ def before_request(self, endpoint: Optional[str] = None) -> Callable[..., None]: :param endpoint str|None: endpoint to server the route. If None, the middleware will be applied to all the routes. """ - - return self.middleware_router.add_middleware(MiddlewareType.BEFORE_REQUEST, endpoint) + return self.middleware_router.add_middleware(MiddlewareType.BEFORE_REQUEST, _normalize_endpoint(endpoint)) def after_request(self, endpoint: Optional[str] = None) -> Callable[..., None]: """ @@ -238,7 +252,7 @@ def after_request(self, endpoint: Optional[str] = None) -> Callable[..., None]: :param endpoint str|None: endpoint to server the route. If None, the middleware will be applied to all the routes. """ - return self.middleware_router.add_middleware(MiddlewareType.AFTER_REQUEST, endpoint) + return self.middleware_router.add_middleware(MiddlewareType.AFTER_REQUEST, _normalize_endpoint(endpoint)) def serve_directory( self, @@ -649,15 +663,22 @@ def __init__(self, file_object: str, prefix: str = "", config: Config = Config() self.prefix = prefix def __add_prefix(self, endpoint: str): - # Normalize both prefix and endpoint to ensure consistent routing - normalized_prefix = _normalize_endpoint(self.prefix) + # Normalize prefix, treating empty as empty (not root) + normalized_prefix = _normalize_endpoint(self.prefix, treat_empty_as_root=True) # Handle empty endpoint - should just be the prefix - if endpoint == "": - return normalized_prefix + if endpoint in ("", "/"): + return normalized_prefix if normalized_prefix else "/" - # Normalize the endpoint and combine with prefix + # Convert root prefix to empty to avoid double slashes when making endpoint + if normalized_prefix == "/": + normalized_prefix = "" # Empty prefix for root + + # Normalize and validate endpoint normalized_endpoint = _normalize_endpoint(endpoint) + if normalized_endpoint is None: + raise ValueError("Endpoint cannot be blank, do specify '/' for root endpoint") + return f"{normalized_prefix}{normalized_endpoint}" def get(self, endpoint: str, const: bool = False, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["get"]): diff --git a/robyn/router.py b/robyn/router.py index 7df5196da..dff065d32 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -336,6 +336,22 @@ def inner_handler(request: Request, *args): # They take a handler, convert it into a closure and return the arguments. # Arguments are returned as they could be modified by the middlewares. def add_middleware(self, middleware_type: MiddlewareType, endpoint: Optional[str]) -> Callable[..., None]: + """ + This method adds a middleware to the router. + + Rules: + If endpoint is None, the middleware is added as a global middleware. + If endpoint is provided, the middleware is added to that specific endpoint. + Only None is supported for global middleware, empty string is not supported. + empty string or "/" is considered as root endpoint. + + Args: + middleware_type: The type of middleware to add (before_request, after_request). + endpoint: The endpoint to add the middleware to. If None, the middleware is added as a global middleware. + + Returns: + A decorator that takes a handler and adds it as a middleware. + """ # no dependency injection here injected_dependencies: dict = {} diff --git a/src/routers/http_router.rs b/src/routers/http_router.rs index 43b31ff9c..b877b19af 100644 --- a/src/routers/http_router.rs +++ b/src/routers/http_router.rs @@ -43,7 +43,7 @@ impl Router<(FunctionInfo, HashMap), HttpMethod> for HttpRouter let table_lock = table.read(); - // First try the original route + // Trying route matching just once. if let Ok(res) = table_lock.at(route) { let mut route_params = HashMap::new(); for (key, value) in res.params.iter() { @@ -54,34 +54,6 @@ impl Router<(FunctionInfo, HashMap), HttpMethod> for HttpRouter return Some((function_info, route_params)); } - // If original route fails, try normalized version (add/remove trailing slash) - let normalized_route = if route.ends_with('/') && route.len() > 1 { - // Remove trailing slash (except for root "/") - &route[..route.len() - 1] - } else { - // Add trailing slash - return table_lock.at(&format!("{}/", route)).ok().map(|res| { - let mut route_params = HashMap::new(); - for (key, value) in res.params.iter() { - route_params.insert(key.to_string(), value.to_string()); - } - - let function_info = Python::with_gil(|_| res.value.to_owned()); - (function_info, route_params) - }); - }; - - // Try the normalized route - if let Ok(res) = table_lock.at(normalized_route) { - let mut route_params = HashMap::new(); - for (key, value) in res.params.iter() { - route_params.insert(key.to_string(), value.to_string()); - } - - let function_info = Python::with_gil(|_| res.value.to_owned()); - return Some((function_info, route_params)); - } - None } } diff --git a/src/server.rs b/src/server.rs index f0b61c1f7..6aab757c4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -485,9 +485,10 @@ async fn index( return ResponseType::Standard(Response::method_not_allowed(None)); } - let mut request = Request::from_actix_request(&req, payload, &global_request_headers).await; + let mut request: Request = + Request::from_actix_request(&req, payload, &global_request_headers).await; - let route = format!("{}{}", req.method(), req.uri().path()); + let route = format!("{}{}", req.method(), request.url.path); // Before middleware // Global @@ -510,7 +511,7 @@ async fn index( Err(e) => { error!( "Error while executing before middleware function for endpoint `{}`: {}", - req.uri().path(), + request.url.path, get_traceback(e.downcast_ref::().unwrap()) ); return ResponseType::Standard(Response::internal_server_error(None)); @@ -524,9 +525,9 @@ async fn index( Err(_) => return ResponseType::Standard(Response::method_not_allowed(None)), }; - let mut response = if let Some(res) = const_router.get_route(&http_method, req.uri().path()) { + let mut response = if let Some(res) = const_router.get_route(&http_method, &request.url.path) { ResponseType::Standard(res) - } else if let Some((function, route_params)) = router.get_route(&http_method, req.uri().path()) + } else if let Some((function, route_params)) = router.get_route(&http_method, &request.url.path) { request.path_params = route_params; match execute_http_function(&request, &function).await { @@ -534,7 +535,7 @@ async fn index( Err(e) => { error!( "Error while executing route function for endpoint `{}`: {}", - req.uri().path(), + request.url.path, get_traceback(&e) ); @@ -552,7 +553,7 @@ async fn index( match &excluded_response_headers_paths.get_ref() { None => {} Some(excluded_response_headers_paths) => { - if excluded_response_headers_paths.contains(&req.uri().path().to_owned()) { + if excluded_response_headers_paths.contains(&request.url.path.to_owned()) { response.headers_mut().clear(); } } @@ -587,7 +588,7 @@ async fn index( Err(e) => { error!( "Error while executing after middleware function for endpoint `{}`: {}", - req.uri().path(), + request.url.path, get_traceback(e.downcast_ref::().unwrap()) ); return ResponseType::Standard(Response::internal_server_error(Some( diff --git a/src/types/request.rs b/src/types/request.rs index 92b7bb114..6e5860d68 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -179,10 +179,21 @@ impl Request { debug!("Request form data: {:?}", form_data); debug!("Request files: {:?}", files); + // Normalizing Path. + // Rules: + // 1. Other than Root("/"), "/endpoint/" will be routed to "/endpoint" internally, without any client redirection. + let route_path = { + let mut path = req.path(); + if path.ends_with("/") && path.len() > 1 { + path = &path[..path.len() - 1] + } + path + }; + let url = Url::new( req.connection_info().scheme(), req.connection_info().host(), - req.path(), + route_path, ); let ip_addr = req.peer_addr().map(|val| val.ip().to_string()); From 3831301149f24f4f43e4368e59e81bafd8caa47d Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 24 Jan 2026 01:21:42 +0000 Subject: [PATCH 022/106] fix: cookies (#1292) * fix: cookies * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update * update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../en/api_reference/getting_started.mdx | 163 +++++++++++- .../zh/api_reference/getting_started.mdx | 161 +++++++++++- integration_tests/base_routes.py | 31 +++ integration_tests/test_get_requests.py | 36 ++- robyn/robyn.pyi | 147 ++++++++++- src/lib.rs | 4 + src/types/cookie.rs | 247 ++++++++++++++++++ src/types/mod.rs | 1 + src/types/response.rs | 58 +++- 9 files changed, 822 insertions(+), 26 deletions(-) create mode 100644 src/types/cookie.rs diff --git a/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx b/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx index 6d03ec4d4..91585f505 100644 --- a/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx @@ -984,28 +984,171 @@ To prevent the headers from getting applied to certain endpoints, you can use th -Set cookies using the `set_cookies` function. +Robyn provides a complete cookie API following RFC 6265. Set cookies using the `set_cookie` method on the Response object. - ```python {{ title: 'untyped' }} + from robyn import Response, Headers + @app.get("/") - def binary_output_response_sync(request): - response = Response(200, {'type': 'int'}, "desc") - response.set_cookie(key="fakesession", value="fake-cookie-session-value") + def set_session(request): + response = Response(200, Headers({}), "Welcome!") + response.set_cookie(key="session", value="abc123") return response ``` - ```python {{title: 'typed'}} - from robyn import Request + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers @app.get("/") - def binary_output_response_sync(request: Request): - response = Response(200, {'type': 'int'}, "desc") - response.set_cookie(key="fakesession", value="fake-cookie-session-value") + def set_session(request: Request): + response = Response(200, Headers({}), "Welcome!") + response.set_cookie(key="session", value="abc123") + return response + ``` + + + + +#### Cookie Attributes + + + +You can set additional cookie attributes for security and control: + +- **path**: Cookie path (default: "/") +- **domain**: Cookie domain +- **max_age**: Cookie lifetime in seconds +- **secure**: Only send over HTTPS +- **http_only**: Not accessible via JavaScript +- **same_site**: CSRF protection ("Strict", "Lax", or "None" - case insensitive) + + + + + + ```python {{ title: 'untyped' }} + @app.get("/login") + def login(request): + response = Response(200, Headers({}), "Logged in") + response.set_cookie( + key="auth_token", + value="secret123", + path="/", + max_age=3600, # 1 hour + secure=True, # HTTPS only + http_only=True, # No JavaScript access + same_site="Strict", # CSRF protection + ) + return response + ``` + + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers + + @app.get("/login") + def login(request: Request): + response = Response(200, Headers({}), "Logged in") + response.set_cookie( + key="auth_token", + value="secret123", + path="/", + max_age=3600, # 1 hour + secure=True, # HTTPS only + http_only=True, # No JavaScript access + same_site="Strict", # CSRF protection + ) + return response + ``` + + + + +#### Deleting Cookies + + + +To delete a cookie from the browser, use the `delete` method on the cookies collection. This sets `max_age=0` which tells the browser to remove the cookie. + + + + + + ```python {{ title: 'untyped' }} + @app.get("/logout") + def logout(request): + response = Response(200, Headers({}), "Logged out") + response.cookies.delete("auth_token") + return response + ``` + + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers + + @app.get("/logout") + def logout(request: Request): + response = Response(200, Headers({}), "Logged out") + response.cookies.delete("auth_token") + return response + ``` + + + + +#### Accessing Cookies + + + +You can iterate over cookies or access them by name: + + + + + + ```python {{ title: 'untyped' }} + @app.get("/debug") + def debug_cookies(request): + response = Response(200, Headers({}), "Cookies set") + response.set_cookie("a", "1") + response.set_cookie("b", "2") + + # Get all cookie names + names = response.cookies.keys() + + # Iterate over cookies + for name in response.cookies: + print(f"Cookie: {name}") + + # Check if cookie exists + if "a" in response.cookies: + print("Cookie 'a' exists") + + return response + ``` + + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers + + @app.get("/debug") + def debug_cookies(request: Request): + response = Response(200, Headers({}), "Cookies set") + response.set_cookie("a", "1") + response.set_cookie("b", "2") + + # Get all cookie names + names = response.cookies.keys() + + # Iterate over cookies + for name in response.cookies: + print(f"Cookie: {name}") + + # Check if cookie exists + if "a" in response.cookies: + print("Cookie 'a' exists") + return response ``` diff --git a/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx b/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx index 97a712be2..d18da118b 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx @@ -612,31 +612,174 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: -使用 `set_cookies` 功能设置 Cookies: +Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对象上的 `set_cookie` 方法来设置 Cookie。 ```python {{ title: 'untyped' }} + from robyn import Response, Headers + @app.get("/") - def binary_output_response_sync(request): - response = Response(200, {'type': 'int'}, "desc") - response.set_cookie(key="fakesession", value="fake-cookie-session-value") + def set_session(request): + response = Response(200, Headers({}), "Welcome!") + response.set_cookie(key="session", value="abc123") return response ``` - ```python {{title: 'typed'}} - from robyn import Request + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers @app.get("/") - def binary_output_response_sync(request: Request): - response = Response(200, {'type': 'int'}, "desc") - response.set_cookie(key="fakesession", value="fake-cookie-session-value") + def set_session(request: Request): + response = Response(200, Headers({}), "Welcome!") + response.set_cookie(key="session", value="abc123") + return response + ``` + + + + +#### Cookie 属性 + + + +您可以设置额外的 Cookie 属性以增强安全性和控制: + +- **path**: Cookie 路径(默认:"/") +- **domain**: Cookie 域名 +- **max_age**: Cookie 有效期(秒) +- **secure**: 仅通过 HTTPS 发送 +- **http_only**: JavaScript 无法访问 +- **same_site**: CSRF 保护("Strict"、"Lax" 或 "None" - 不区分大小写) + + + + + + ```python {{ title: 'untyped' }} + @app.get("/login") + def login(request): + response = Response(200, Headers({}), "Logged in") + response.set_cookie( + key="auth_token", + value="secret123", + path="/", + max_age=3600, # 1 小时 + secure=True, # 仅 HTTPS + http_only=True, # 禁止 JavaScript 访问 + same_site="Strict", # CSRF 保护 + ) + return response + ``` + + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers + + @app.get("/login") + def login(request: Request): + response = Response(200, Headers({}), "Logged in") + response.set_cookie( + key="auth_token", + value="secret123", + path="/", + max_age=3600, # 1 小时 + secure=True, # 仅 HTTPS + http_only=True, # 禁止 JavaScript 访问 + same_site="Strict", # CSRF 保护 + ) + return response + ``` + + + + +#### 删除 Cookie + + + +要从浏览器删除 Cookie,请使用 cookies 集合上的 `delete` 方法。这会设置 `max_age=0`,告诉浏览器删除该 Cookie。 + + + + + + ```python {{ title: 'untyped' }} + @app.get("/logout") + def logout(request): + response = Response(200, Headers({}), "Logged out") + response.cookies.delete("auth_token") + return response + ``` + + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers + + @app.get("/logout") + def logout(request: Request): + response = Response(200, Headers({}), "Logged out") + response.cookies.delete("auth_token") return response ``` + + +#### 访问 Cookie + + + +您可以遍历 Cookie 或按名称访问它们: + + + + + + ```python {{ title: 'untyped' }} + @app.get("/debug") + def debug_cookies(request): + response = Response(200, Headers({}), "Cookies set") + response.set_cookie("a", "1") + response.set_cookie("b", "2") + + # 获取所有 Cookie 名称 + names = response.cookies.keys() + + # 遍历 Cookie + for name in response.cookies: + print(f"Cookie: {name}") + + # 检查 Cookie 是否存在 + if "a" in response.cookies: + print("Cookie 'a' exists") + + return response + ``` + + ```python {{ title: 'typed' }} + from robyn import Request, Response, Headers + + @app.get("/debug") + def debug_cookies(request: Request): + response = Response(200, Headers({}), "Cookies set") + response.set_cookie("a", "1") + response.set_cookie("b", "2") + + # 获取所有 Cookie 名称 + names = response.cookies.keys() + + # 遍历 Cookie + for name in response.cookies: + print(f"Cookie: {name}") + + # 检查 Cookie 是否存在 + if "a" in response.cookies: + print("Cookie 'a' exists") + + return response + ``` + diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index becffa93a..6cf22a2a1 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -562,6 +562,37 @@ def cookie(): return response +@app.get("/cookie/multiple") +def multiple_cookies(): + response = Response(status_code=200, headers=Headers({}), description="test multiple cookies") + response.set_cookie(key="session", value="abc123") + response.set_cookie(key="theme", value="dark") + return response + + +@app.get("/cookie/attributes") +def cookie_with_attributes(): + response = Response(status_code=200, headers=Headers({}), description="test cookie attributes") + response.set_cookie( + key="secure_session", + value="secret123", + path="/", + http_only=True, + secure=True, + same_site="Strict", + max_age=3600, + ) + return response + + +@app.get("/cookie/overwrite") +def cookie_overwrite(): + response = Response(status_code=200, headers=Headers({}), description="test cookie overwrite") + response.set_cookie(key="session", value="first-value") + response.set_cookie(key="session", value="final-value") # Should overwrite + return response + + # --- POST --- # dict diff --git a/integration_tests/test_get_requests.py b/integration_tests/test_get_requests.py index 8afa15af9..1281fb3e9 100644 --- a/integration_tests/test_get_requests.py +++ b/integration_tests/test_get_requests.py @@ -68,4 +68,38 @@ def test_trailing_slash(session): def test_cookies(session, key, value): response = get("/cookie", 200) - assert response.headers[key] == value + # Cookies should be sent via Set-Cookie header, accessible via response.cookies + assert response.cookies[key] == value + + +@pytest.mark.benchmark +def test_multiple_cookies(session): + response = get("/cookie/multiple", 200) + + assert response.cookies["session"] == "abc123" + assert response.cookies["theme"] == "dark" + + +@pytest.mark.benchmark +def test_cookie_with_attributes(session): + response = get("/cookie/attributes", 200) + + # Check the cookie value + assert response.cookies["secure_session"] == "secret123" + + # Check the Set-Cookie header for attributes + set_cookie_header = response.headers.get("Set-Cookie", "") + assert "secure_session=secret123" in set_cookie_header + assert "Path=/" in set_cookie_header + assert "HttpOnly" in set_cookie_header + assert "Secure" in set_cookie_header + assert "SameSite=Strict" in set_cookie_header + assert "Max-Age=3600" in set_cookie_header + + +@pytest.mark.benchmark +def test_cookie_overwrite(session): + response = get("/cookie/overwrite", 200) + + # Same-name cookies should be overwritten, final value should be used + assert response.cookies["session"] == "final-value" diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi index 7c613c8ed..0714b345c 100644 --- a/robyn/robyn.pyi +++ b/robyn/robyn.pyi @@ -176,6 +176,126 @@ class QueryParams: def __repr__(self) -> str: pass +@dataclass +class Cookie: + """ + A cookie with optional attributes following RFC 6265. + + Attributes: + value (str): The cookie value + path (Optional[str]): Cookie path (e.g. "/") + domain (Optional[str]): Cookie domain + max_age (Optional[int]): Max age in seconds + expires (Optional[str]): Expiry date (deprecated, use max_age instead) + secure (bool): Only send over HTTPS + http_only (bool): Not accessible via JavaScript + same_site (Optional[str]): "Strict", "Lax", or "None" (case-insensitive) + """ + + value: str + path: Optional[str] = None + domain: Optional[str] = None + max_age: Optional[int] = None + expires: Optional[str] = None + secure: bool = False + http_only: bool = False + same_site: Optional[str] = None + + @staticmethod + def deleted() -> "Cookie": + """ + Create a cookie configured for deletion (expires immediately with max_age=0). + + Returns: + Cookie: A cookie that will be deleted by the browser + """ + pass + +class Cookies: + """A collection of cookies keyed by name.""" + + def __init__(self) -> None: + pass + + def set(self, name: str, cookie: Cookie) -> None: + """ + Sets a cookie with the given name. + + Args: + name (str): The name of the cookie + cookie (Cookie): The cookie object + """ + pass + + def get(self, name: str) -> Optional[Cookie]: + """ + Gets the cookie with the given name. + + Args: + name (str): The name of the cookie + """ + pass + + def remove(self, name: str) -> None: + """ + Removes the cookie from the collection (does not delete it from the browser). + + Args: + name (str): The name of the cookie + """ + pass + + def delete(self, name: str) -> None: + """ + Mark a cookie for deletion by setting it to expire immediately. + This sets max_age=0 which tells the browser to delete the cookie. + + Args: + name (str): The name of the cookie to delete + """ + pass + + def is_empty(self) -> bool: + """ + Returns: + True if there are no cookies, False otherwise + """ + pass + + def keys(self) -> list[str]: + """ + Returns: + A list of all cookie names + """ + pass + + def __setitem__(self, name: str, cookie: Cookie) -> None: + pass + + def __getitem__(self, name: str) -> Optional[Cookie]: + pass + + def __contains__(self, name: str) -> bool: + pass + + def __len__(self) -> int: + pass + + def __iter__(self) -> "CookiesIter": + pass + + def __repr__(self) -> str: + pass + +class CookiesIter: + """Iterator for Cookies collection.""" + + def __iter__(self) -> "CookiesIter": + pass + + def __next__(self) -> str: + pass + class Headers: def __init__(self, default_headers: Optional[dict]) -> None: pass @@ -289,6 +409,7 @@ class Response: headers (Union[Headers, dict]): The headers of the response or Headers directly. e.g. {"Content-Type": "application/json"} description (Union[str, bytes]): The body of the response. If the response is a JSON, it will be a dict. file_path (Optional[str]): The file path of the response. e.g. /home/user/file.txt + cookies (Cookies): The cookies to set in the response. """ status_code: int @@ -296,14 +417,34 @@ class Response: description: Union[str, bytes] response_type: Optional[str] = None file_path: Optional[str] = None + cookies: Cookies = None # Initialized automatically - def set_cookie(self, key: str, value: str) -> None: + def set_cookie( + self, + key: str, + value: str, + path: Optional[str] = None, + domain: Optional[str] = None, + max_age: Optional[int] = None, + expires: Optional[str] = None, + secure: bool = False, + http_only: bool = False, + same_site: Optional[str] = None, + ) -> None: """ - Sets the cookie in the response. + Sets a cookie in the response. If a cookie with the same key exists, + it will be overwritten. Args: - key (str): The key of the cookie + key (str): The name of the cookie value (str): The value of the cookie + path (Optional[str]): Cookie path (e.g. "/") + domain (Optional[str]): Cookie domain + max_age (Optional[int]): Max age in seconds + expires (Optional[str]): Expiry date (RFC 7231 format) + secure (bool): Only send over HTTPS + http_only (bool): Not accessible via JavaScript + same_site (Optional[str]): "Strict", "Lax", or "None" """ pass diff --git a/src/lib.rs b/src/lib.rs index db59c80ea..3bf2e795b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ use shared_socket::SocketHeld; // pyO3 module use pyo3::prelude::*; use types::{ + cookie::{Cookie, Cookies, CookiesIter}, function_info::{FunctionInfo, MiddlewareType}, headers::Headers, identity::Identity, @@ -35,6 +36,9 @@ pub fn robyn(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/types/cookie.rs b/src/types/cookie.rs new file mode 100644 index 000000000..78a2c2c6b --- /dev/null +++ b/src/types/cookie.rs @@ -0,0 +1,247 @@ +use actix_web::cookie::{Cookie as ActixCookie, SameSite}; +use log::debug; +use pyo3::prelude::*; +use std::collections::HashMap; +use std::fmt; + +/// Error type for cookie validation failures +#[derive(Debug, Clone)] +pub enum CookieError { + InvalidSameSite(String), +} + +impl fmt::Display for CookieError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CookieError::InvalidSameSite(msg) => write!(f, "Invalid SameSite value: {}", msg), + } + } +} + +impl std::error::Error for CookieError {} + +/// Parse SameSite value (case-insensitive) +fn parse_same_site(value: &str) -> Result { + match value.to_lowercase().as_str() { + "strict" => Ok(SameSite::Strict), + "lax" => Ok(SameSite::Lax), + "none" => Ok(SameSite::None), + _ => Err(CookieError::InvalidSameSite(format!( + "must be 'Strict', 'Lax', or 'None', got '{}'", + value + ))), + } +} + +/// A cookie with optional attributes following RFC 6265 +#[pyclass(name = "Cookie")] +#[derive(Debug, Clone)] +pub struct Cookie { + #[pyo3(get, set)] + pub value: String, + #[pyo3(get, set)] + pub path: Option, + #[pyo3(get, set)] + pub domain: Option, + #[pyo3(get, set)] + pub max_age: Option, + #[pyo3(get, set)] + pub expires: Option, + #[pyo3(get, set)] + pub secure: bool, + #[pyo3(get, set)] + pub http_only: bool, + #[pyo3(get, set)] + pub same_site: Option, +} + +#[pymethods] +impl Cookie { + #[new] + #[pyo3(signature = (value, path=None, domain=None, max_age=None, expires=None, secure=false, http_only=false, same_site=None))] + pub fn new( + value: String, + path: Option, + domain: Option, + max_age: Option, + expires: Option, + secure: bool, + http_only: bool, + same_site: Option, + ) -> Self { + Self { + value, + path, + domain, + max_age, + expires, + secure, + http_only, + same_site, + } + } + + /// Create a cookie configured for deletion (expires immediately with max_age=0) + #[staticmethod] + pub fn deleted() -> Self { + Self { + value: String::new(), + path: Some("/".to_string()), + domain: None, + max_age: Some(0), + expires: None, + secure: false, + http_only: false, + same_site: None, + } + } + + fn __repr__(&self) -> String { + format!( + "Cookie(value={:?}, path={:?}, domain={:?}, max_age={:?}, expires={:?}, secure={}, http_only={}, same_site={:?})", + self.value, self.path, self.domain, self.max_age, self.expires, self.secure, self.http_only, self.same_site + ) + } +} + +impl Cookie { + /// Serialize cookie to Set-Cookie header value format. + /// + /// Uses actix-web's cookie crate for RFC 6265 compliant serialization + /// which handles proper validation and encoding of cookie values. + /// + /// Returns an error if SameSite has an invalid value. + pub fn to_header_value(&self, name: &str) -> Result { + let mut builder = ActixCookie::build(name, &self.value); + + if let Some(ref path) = self.path { + builder = builder.path(path.clone()); + } + if let Some(ref domain) = self.domain { + builder = builder.domain(domain.clone()); + } + if let Some(max_age) = self.max_age { + builder = builder.max_age(actix_web::cookie::time::Duration::seconds(max_age)); + } + // Note: expires is skipped as max_age is the modern/preferred approach + // The expires field is kept for API compatibility but not used in serialization + if self.expires.is_some() { + debug!("Cookie 'expires' attribute is deprecated; use 'max_age' instead"); + } + if self.secure { + builder = builder.secure(true); + } + if self.http_only { + builder = builder.http_only(true); + } + if let Some(ref same_site) = self.same_site { + builder = builder.same_site(parse_same_site(same_site)?); + } + + Ok(builder.finish().to_string()) + } +} + +/// A collection of cookies keyed by name +#[pyclass(name = "Cookies")] +#[derive(Debug, Clone, Default)] +pub struct Cookies { + pub cookies: HashMap, +} + +#[pymethods] +impl Cookies { + #[new] + pub fn new() -> Self { + Self { + cookies: HashMap::new(), + } + } + + /// Set a cookie with the given name + pub fn set(&mut self, name: String, cookie: Cookie) { + self.cookies.insert(name, cookie); + } + + /// Get a cookie by name + pub fn get(&self, name: String) -> Option { + self.cookies.get(&name).cloned() + } + + /// Remove a cookie from the collection (does not delete it from the browser) + pub fn remove(&mut self, name: &str) { + self.cookies.remove(name); + } + + /// Mark a cookie for deletion by setting it to expire immediately. + /// This sets max_age=0 which tells the browser to delete the cookie. + pub fn delete(&mut self, name: String) { + self.cookies.insert(name, Cookie::deleted()); + } + + /// Check if the collection is empty + pub fn is_empty(&self) -> bool { + self.cookies.is_empty() + } + + /// Get the number of cookies + pub fn len(&self) -> usize { + self.cookies.len() + } + + /// Get all cookie names + pub fn keys(&self) -> Vec { + self.cookies.keys().cloned().collect() + } + + pub fn __setitem__(&mut self, name: String, cookie: Cookie) { + self.set(name, cookie); + } + + pub fn __getitem__(&self, name: String) -> Option { + self.get(name) + } + + pub fn __contains__(&self, name: String) -> bool { + self.cookies.contains_key(&name) + } + + pub fn __len__(&self) -> usize { + self.len() + } + + pub fn __iter__(slf: PyRef<'_, Self>) -> CookiesIter { + CookiesIter { + keys: slf.cookies.keys().cloned().collect(), + index: 0, + } + } + + fn __repr__(&self) -> String { + format!("{:?}", self.cookies) + } +} + +/// Iterator for Cookies collection +#[pyclass] +pub struct CookiesIter { + keys: Vec, + index: usize, +} + +#[pymethods] +impl CookiesIter { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.index < slf.keys.len() { + let key = slf.keys[slf.index].clone(); + slf.index += 1; + Some(key) + } else { + None + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 2fd1b6b8d..e04361fe5 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,6 +5,7 @@ use pyo3::{ types::{PyBytes, PyString}, }; +pub mod cookie; pub mod function_info; pub mod headers; pub mod identity; diff --git a/src/types/response.rs b/src/types/response.rs index c85670e0c..389c91fc5 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -14,6 +14,7 @@ use tokio; use crate::io_helpers::{apply_hashmap_headers, read_file}; use crate::types::{check_body_type, check_description_type, get_description_from_pyobject}; +use super::cookie::{Cookie, Cookies}; use super::headers::Headers; #[derive(Debug, Clone)] @@ -23,6 +24,7 @@ pub struct Response { pub headers: Headers, pub description: Vec, pub file_path: Option, + pub cookies: Cookies, } #[derive(Debug, Clone)] @@ -66,6 +68,19 @@ impl Responder for Response { StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), ); apply_hashmap_headers(&mut response_builder, &self.headers); + + // Apply cookies as Set-Cookie headers + for (name, cookie) in &self.cookies.cookies { + match cookie.to_header_value(name) { + Ok(header_value) => { + response_builder.append_header(("Set-Cookie", header_value)); + } + Err(e) => { + debug!("Skipping invalid cookie '{}': {}", name, e); + } + } + } + response_builder.body(self.description) } } @@ -179,6 +194,7 @@ impl Response { headers: headers.cloned().unwrap_or_else(|| Headers::new(None)), description: NOT_FOUND_BYTES.to_vec(), file_path: None, + cookies: Cookies::new(), } } @@ -191,6 +207,7 @@ impl Response { headers: headers.cloned().unwrap_or_else(|| Headers::new(None)), description: SERVER_ERROR_BYTES.to_vec(), file_path: None, + cookies: Cookies::new(), } } @@ -203,6 +220,7 @@ impl Response { headers: headers.cloned().unwrap_or_else(|| Headers::new(None)), description: METHOD_NOT_ALLOWED_BYTES.to_vec(), file_path: None, + cookies: Cookies::new(), } } } @@ -223,12 +241,15 @@ impl<'py> IntoPyObject<'py> for Response { } }; + let cookies: Py = Py::new(py, self.cookies)?; + let response = PyResponse { status_code: self.status_code, response_type: self.response_type, headers, description: description.into(), file_path: self.file_path, + cookies, }; Ok(Py::new(py, response)?.into_bound(py).into_any()) } @@ -247,6 +268,8 @@ pub struct PyResponse { pub description: Py, #[pyo3(get)] pub file_path: Option, + #[pyo3(get)] + pub cookies: Py, } #[pyclass(name = "StreamingResponse")] @@ -339,6 +362,8 @@ impl PyResponse { )); }; + let cookies: Py = Py::new(py, Cookies::new())?; + Ok(Self { status_code, // we should be handling based on headers but works for now @@ -346,6 +371,7 @@ impl PyResponse { headers: headers_output, description, file_path: None, + cookies, }) } @@ -370,11 +396,35 @@ impl PyResponse { } } - pub fn set_cookie(&mut self, py: Python, key: &str, value: &str) -> PyResult<()> { - self.headers + #[pyo3(signature = (key, value, path=None, domain=None, max_age=None, expires=None, secure=false, http_only=false, same_site=None))] + pub fn set_cookie( + &mut self, + py: Python, + key: &str, + value: &str, + path: Option, + domain: Option, + max_age: Option, + expires: Option, + secure: bool, + http_only: bool, + same_site: Option, + ) -> PyResult<()> { + let cookie = Cookie::new( + value.to_string(), + path, + domain, + max_age, + expires, + secure, + http_only, + same_site, + ); + + self.cookies .try_borrow_mut(py) .expect("value already borrowed") - .append(key.to_string(), value.to_string()); + .set(key.to_string(), cookie); Ok(()) } } @@ -399,6 +449,7 @@ impl FromPyObject<'_, '_> for Response { let headers: Headers = obj.getattr("headers")?.extract()?; let description: Vec = get_description_from_pyobject(&obj.getattr("description")?)?; let file_path: Option = obj.getattr("file_path")?.extract()?; + let cookies: Cookies = obj.getattr("cookies")?.extract()?; debug!( "Successfully extracted Response with status {}", @@ -410,6 +461,7 @@ impl FromPyObject<'_, '_> for Response { headers, description, file_path, + cookies, }) } } From 3bc155c1063c3a72a308775dd09fae45693da0bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:26:58 +0000 Subject: [PATCH 023/106] [pre-commit.ci] pre-commit autoupdate (#1291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.14.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.14.13) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cdbd712a..89a3018a1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.10 + rev: v0.14.13 hooks: - id: ruff args: From 14ac227ac2698689ed70bebb57d7a188b885588c Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sun, 25 Jan 2026 13:56:44 +0000 Subject: [PATCH 024/106] Release 0.76.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f33ca5a87..c8a77a6c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.75.0" +version = "0.76.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index c9e11bb19..785b55633 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.75.0" +version = "0.76.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index 45651b505..61c6edbf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.75.0" +version = "0.76.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -67,7 +67,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.75.0" +version = "0.76.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 35fbeb7d90a573f431dfea497061217feb4bd0e6 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:28:02 +0000 Subject: [PATCH 025/106] feat: add llms.txt (#1294) --- docs_src/public/llms.txt | 242 +++++++++++++++++++++++++++++++++++++++ llms.txt | 242 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 484 insertions(+) create mode 100644 docs_src/public/llms.txt create mode 100644 llms.txt diff --git a/docs_src/public/llms.txt b/docs_src/public/llms.txt new file mode 100644 index 000000000..3a3adcc73 --- /dev/null +++ b/docs_src/public/llms.txt @@ -0,0 +1,242 @@ +# Robyn + +> Robyn is a high-performance, community-driven, and innovator-friendly async web framework for Python with a Rust runtime. It combines Python's ease of use with Rust's performance. + +## Quick Facts + +- Version: 0.76.0 +- Python: >= 3.10 +- License: BSD 2.0 +- Repository: https://github.com/sparckles/robyn +- Documentation: https://robyn.tech/documentation +- Discord: https://discord.gg/rkERZ5eNU8 + +## Installation + +```bash +pip install robyn +``` + +## Basic Usage + +```python +from robyn import Robyn + +app = Robyn(__file__) + +@app.get("/") +async def index(request): + return "Hello, World!" + +app.start(port=8080) +``` + +## Key Features + +- **Rust Runtime**: Core server written in Rust using actix-web for high performance +- **Async/Sync Support**: Both async and sync route handlers supported +- **Multi-Process Scaling**: Built-in multiprocess execution via `--processes` and `--workers` +- **WebSockets**: Native WebSocket support +- **Middlewares**: Before/after request middlewares +- **Dependency Injection**: Built-in DI system +- **OpenAPI/Swagger**: Automatic OpenAPI documentation generation +- **Hot Reloading**: Development mode with `--dev` flag +- **AI Agents**: Built-in AI agent routing via `robyn.ai` +- **MCP Support**: Model Context Protocol server capabilities via `app.mcp` +- **Templating**: Jinja2 templating support (optional) +- **CORS**: Built-in CORS helper via `ALLOW_CORS()` +- **Authentication**: AuthenticationHandler base class for custom auth +- **Static Files**: Directory serving via `app.serve_directory()` +- **SSE**: Server-Sent Events support via `SSEResponse` +- **Direct Rust Integration**: Embed Rust code directly in routes + +## Project Structure + +``` +robyn/ +├── src/ # Rust source code +│ ├── lib.rs # PyO3 module entry point +│ ├── server.rs # Main HTTP server implementation +│ ├── types/ # Request, Response, Headers, Cookie types +│ ├── routers/ # HTTP, WebSocket, middleware routers +│ ├── executors/ # Route execution handlers +│ └── websockets/ # WebSocket implementation +├── robyn/ # Python package +│ ├── __init__.py # Main Robyn and SubRouter classes +│ ├── router.py # Python router implementation +│ ├── authentication.py # AuthenticationHandler +│ ├── dependency_injection.py +│ ├── openapi.py # OpenAPI generation +│ ├── mcp.py # MCP protocol support +│ ├── ai.py # AI agent support +│ ├── responses.py # Response helpers (serve_file, html, SSE) +│ ├── ws.py # WebSocket class +│ └── robyn.pyi # Type stubs +├── integration_tests/ # Integration test suite +├── unit_tests/ # Unit test suite +├── docs_src/ # Documentation (Next.js) +├── granian/ # Bundled Granian server (fork) +└── examples/ # Example applications +``` + +## Core Classes + +### Robyn / SubRouter +Main application class and sub-router for modular routes. + +```python +from robyn import Robyn, SubRouter + +app = Robyn(__file__) +api = SubRouter(__file__, prefix="/api") + +@api.get("/users") +def get_users(request): + return {"users": []} + +app.include_router(api) +``` + +### Request Object +```python +request.method # HTTP method +request.url # Url object (scheme, host, path) +request.headers # Headers dict-like +request.query_params # QueryParams +request.path_params # Dict of URL params +request.body # Raw bytes +request.json() # Parse JSON body +request.form_data # Multipart form data +request.ip_addr # Client IP +request.identity # Identity (if authenticated) +``` + +### Response Object +```python +from robyn import Response + +Response( + status_code=200, + headers={"Content-Type": "application/json"}, + description="body content" # or body bytes +) +``` + +### Decorators +```python +@app.get("/path") +@app.post("/path") +@app.put("/path") +@app.delete("/path") +@app.patch("/path") +@app.head("/path") +@app.options("/path") + +@app.before_request("/path") # Middleware before +@app.after_request("/path") # Middleware after + +@app.startup_handler # Server startup +@app.shutdown_handler # Server shutdown +``` + +### WebSockets +```python +from robyn import WebSocket + +ws = WebSocket(app, "/ws") + +@ws.on("connect") +def on_connect(ws, msg): + return "Connected" + +@ws.on("message") +def on_message(ws, msg): + return f"Echo: {msg}" + +@ws.on("close") +def on_close(ws, msg): + return "Closed" +``` + +### MCP (Model Context Protocol) +```python +@app.mcp.resource("time://current") +def get_time(): + return datetime.now().isoformat() + +@app.mcp.tool(name="calc", description="Calculate", input_schema={...}) +def calculate(args): + return eval(args["expression"]) + +@app.mcp.prompt(name="explain", description="Explain code", arguments=[...]) +def explain_prompt(args): + return f"Please explain: {args['code']}" +``` + +## CLI Commands + +```bash +python app.py # Start server +python app.py --dev # Development mode (hot reload) +python app.py --processes 4 # Multi-process +python app.py --workers 2 # Workers per process +python app.py --log-level DEBUG # Log level +python app.py --open-browser # Open browser on start +python app.py --create # Create new project scaffold +python app.py --docs # Open documentation +``` + +## Development Setup + +```bash +# Clone +git clone https://github.com/sparckles/robyn.git +cd robyn + +# Virtual environment +python3 -m venv .venv && source .venv/bin/activate + +# Install tools +pip install pre-commit poetry maturin + +# Install dependencies +poetry install --with dev --with test + +# Build Rust extension +maturin develop + +# Run tests +pytest +``` + +## Key Dependencies + +- **PyO3**: Rust-Python bindings +- **actix-web**: Rust HTTP server (via cookie crate) +- **orjson**: Fast JSON serialization +- **multiprocess**: Multi-process support +- **uvloop**: Fast event loop (non-Windows) +- **watchdog**: File watching for hot reload + +## Configuration + +Environment variables: +- `ROBYN_HOST`: Server host (default: 127.0.0.1) +- `ROBYN_PORT`: Server port (default: 8080) +- `ROBYN_DEV_MODE`: Enable dev mode +- `ROBYN_BROWSER_OPEN`: Open browser on start +- `ROBYN_CLIENT_TIMEOUT`: Client timeout seconds +- `ROBYN_KEEP_ALIVE_TIMEOUT`: Keep-alive timeout + +## Documentation Structure + +Main docs at `docs_src/src/pages/documentation/`: +- `api_reference/getting_started.mdx` - Quick start guide +- `api_reference/request_object.mdx` - Request handling +- `api_reference/middlewares.mdx` - Middleware usage +- `api_reference/websockets.mdx` - WebSocket guide +- `api_reference/authentication.mdx` - Auth patterns +- `api_reference/openapi.mdx` - OpenAPI docs +- `api_reference/agents.mdx` - AI agent integration +- `api_reference/mcps.mdx` - MCP server guide +- `example_app/` - Full example application tutorial diff --git a/llms.txt b/llms.txt new file mode 100644 index 000000000..3a3adcc73 --- /dev/null +++ b/llms.txt @@ -0,0 +1,242 @@ +# Robyn + +> Robyn is a high-performance, community-driven, and innovator-friendly async web framework for Python with a Rust runtime. It combines Python's ease of use with Rust's performance. + +## Quick Facts + +- Version: 0.76.0 +- Python: >= 3.10 +- License: BSD 2.0 +- Repository: https://github.com/sparckles/robyn +- Documentation: https://robyn.tech/documentation +- Discord: https://discord.gg/rkERZ5eNU8 + +## Installation + +```bash +pip install robyn +``` + +## Basic Usage + +```python +from robyn import Robyn + +app = Robyn(__file__) + +@app.get("/") +async def index(request): + return "Hello, World!" + +app.start(port=8080) +``` + +## Key Features + +- **Rust Runtime**: Core server written in Rust using actix-web for high performance +- **Async/Sync Support**: Both async and sync route handlers supported +- **Multi-Process Scaling**: Built-in multiprocess execution via `--processes` and `--workers` +- **WebSockets**: Native WebSocket support +- **Middlewares**: Before/after request middlewares +- **Dependency Injection**: Built-in DI system +- **OpenAPI/Swagger**: Automatic OpenAPI documentation generation +- **Hot Reloading**: Development mode with `--dev` flag +- **AI Agents**: Built-in AI agent routing via `robyn.ai` +- **MCP Support**: Model Context Protocol server capabilities via `app.mcp` +- **Templating**: Jinja2 templating support (optional) +- **CORS**: Built-in CORS helper via `ALLOW_CORS()` +- **Authentication**: AuthenticationHandler base class for custom auth +- **Static Files**: Directory serving via `app.serve_directory()` +- **SSE**: Server-Sent Events support via `SSEResponse` +- **Direct Rust Integration**: Embed Rust code directly in routes + +## Project Structure + +``` +robyn/ +├── src/ # Rust source code +│ ├── lib.rs # PyO3 module entry point +│ ├── server.rs # Main HTTP server implementation +│ ├── types/ # Request, Response, Headers, Cookie types +│ ├── routers/ # HTTP, WebSocket, middleware routers +│ ├── executors/ # Route execution handlers +│ └── websockets/ # WebSocket implementation +├── robyn/ # Python package +│ ├── __init__.py # Main Robyn and SubRouter classes +│ ├── router.py # Python router implementation +│ ├── authentication.py # AuthenticationHandler +│ ├── dependency_injection.py +│ ├── openapi.py # OpenAPI generation +│ ├── mcp.py # MCP protocol support +│ ├── ai.py # AI agent support +│ ├── responses.py # Response helpers (serve_file, html, SSE) +│ ├── ws.py # WebSocket class +│ └── robyn.pyi # Type stubs +├── integration_tests/ # Integration test suite +├── unit_tests/ # Unit test suite +├── docs_src/ # Documentation (Next.js) +├── granian/ # Bundled Granian server (fork) +└── examples/ # Example applications +``` + +## Core Classes + +### Robyn / SubRouter +Main application class and sub-router for modular routes. + +```python +from robyn import Robyn, SubRouter + +app = Robyn(__file__) +api = SubRouter(__file__, prefix="/api") + +@api.get("/users") +def get_users(request): + return {"users": []} + +app.include_router(api) +``` + +### Request Object +```python +request.method # HTTP method +request.url # Url object (scheme, host, path) +request.headers # Headers dict-like +request.query_params # QueryParams +request.path_params # Dict of URL params +request.body # Raw bytes +request.json() # Parse JSON body +request.form_data # Multipart form data +request.ip_addr # Client IP +request.identity # Identity (if authenticated) +``` + +### Response Object +```python +from robyn import Response + +Response( + status_code=200, + headers={"Content-Type": "application/json"}, + description="body content" # or body bytes +) +``` + +### Decorators +```python +@app.get("/path") +@app.post("/path") +@app.put("/path") +@app.delete("/path") +@app.patch("/path") +@app.head("/path") +@app.options("/path") + +@app.before_request("/path") # Middleware before +@app.after_request("/path") # Middleware after + +@app.startup_handler # Server startup +@app.shutdown_handler # Server shutdown +``` + +### WebSockets +```python +from robyn import WebSocket + +ws = WebSocket(app, "/ws") + +@ws.on("connect") +def on_connect(ws, msg): + return "Connected" + +@ws.on("message") +def on_message(ws, msg): + return f"Echo: {msg}" + +@ws.on("close") +def on_close(ws, msg): + return "Closed" +``` + +### MCP (Model Context Protocol) +```python +@app.mcp.resource("time://current") +def get_time(): + return datetime.now().isoformat() + +@app.mcp.tool(name="calc", description="Calculate", input_schema={...}) +def calculate(args): + return eval(args["expression"]) + +@app.mcp.prompt(name="explain", description="Explain code", arguments=[...]) +def explain_prompt(args): + return f"Please explain: {args['code']}" +``` + +## CLI Commands + +```bash +python app.py # Start server +python app.py --dev # Development mode (hot reload) +python app.py --processes 4 # Multi-process +python app.py --workers 2 # Workers per process +python app.py --log-level DEBUG # Log level +python app.py --open-browser # Open browser on start +python app.py --create # Create new project scaffold +python app.py --docs # Open documentation +``` + +## Development Setup + +```bash +# Clone +git clone https://github.com/sparckles/robyn.git +cd robyn + +# Virtual environment +python3 -m venv .venv && source .venv/bin/activate + +# Install tools +pip install pre-commit poetry maturin + +# Install dependencies +poetry install --with dev --with test + +# Build Rust extension +maturin develop + +# Run tests +pytest +``` + +## Key Dependencies + +- **PyO3**: Rust-Python bindings +- **actix-web**: Rust HTTP server (via cookie crate) +- **orjson**: Fast JSON serialization +- **multiprocess**: Multi-process support +- **uvloop**: Fast event loop (non-Windows) +- **watchdog**: File watching for hot reload + +## Configuration + +Environment variables: +- `ROBYN_HOST`: Server host (default: 127.0.0.1) +- `ROBYN_PORT`: Server port (default: 8080) +- `ROBYN_DEV_MODE`: Enable dev mode +- `ROBYN_BROWSER_OPEN`: Open browser on start +- `ROBYN_CLIENT_TIMEOUT`: Client timeout seconds +- `ROBYN_KEEP_ALIVE_TIMEOUT`: Keep-alive timeout + +## Documentation Structure + +Main docs at `docs_src/src/pages/documentation/`: +- `api_reference/getting_started.mdx` - Quick start guide +- `api_reference/request_object.mdx` - Request handling +- `api_reference/middlewares.mdx` - Middleware usage +- `api_reference/websockets.mdx` - WebSocket guide +- `api_reference/authentication.mdx` - Auth patterns +- `api_reference/openapi.mdx` - OpenAPI docs +- `api_reference/agents.mdx` - AI agent integration +- `api_reference/mcps.mdx` - MCP server guide +- `example_app/` - Full example application tutorial From 27a97b0ca2e53fc4cfd5bd5f8d2493f395c29ccb Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:47:09 +0000 Subject: [PATCH 026/106] fix: SSEResponse duplicate CORS headers issue (#1287) * fix: SSEResponse duplicate CORS headers issue * update * fix tests * update * update --- integration_tests/test_sse.py | 8 +++++--- robyn/responses.py | 2 -- src/types/response.rs | 20 -------------------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/integration_tests/test_sse.py b/integration_tests/test_sse.py index fd81ee7dc..4ac795fb6 100644 --- a/integration_tests/test_sse.py +++ b/integration_tests/test_sse.py @@ -17,7 +17,6 @@ def test_sse_basic_headers(session): # Accept either clean optimized headers or legacy compatibility cache_control = response.headers.get("Cache-Control") assert cache_control in ["no-cache, no-store, must-revalidate", "no-cache, no-cache, no-store, must-revalidate"] - assert "Access-Control-Allow-Origin" in response.headers @pytest.mark.benchmark @@ -196,13 +195,17 @@ def test_sse_empty_stream(session): @pytest.mark.benchmark def test_sse_custom_headers(session): - """Test SSE endpoint with custom headers""" + """Test SSE endpoint with custom headers; SSE responses should not include default CORS headers for cross-origin EventSource support""" response = requests.get(f"{BASE_URL}/sse/with_headers", stream=True) assert response.status_code == 200 assert response.headers.get("X-Custom-Header") == "custom-value" assert response.headers.get("Content-Type") == "text/event-stream" + # SSE responses should not include default CORS headers + assert response.headers.get("Access-Control-Allow-Origin") is None + assert response.headers.get("Access-Control-Allow-Headers") is None + @pytest.mark.benchmark def test_sse_custom_status_code(session): @@ -409,7 +412,6 @@ def test_sse_optimization_headers(session): assert response.headers.get("Pragma") == "no-cache" assert response.headers.get("Expires") == "0" assert response.headers.get("X-Accel-Buffering") == "no" # Nginx buffering disabled - assert response.headers.get("Access-Control-Allow-Origin") == "*" # Connection header might be managed by underlying HTTP infrastructure connection = response.headers.get("Connection") assert connection is None or connection == "keep-alive" diff --git a/robyn/responses.py b/robyn/responses.py index 4006567c5..8d3717f11 100644 --- a/robyn/responses.py +++ b/robyn/responses.py @@ -146,8 +146,6 @@ def __init__( if media_type == "text/event-stream": self.headers.set("Content-Type", "text/event-stream") # Cache-Control and Connection headers are set by Rust layer with optimized headers - self.headers.set("Access-Control-Allow-Origin", "*") - self.headers.set("Access-Control-Allow-Headers", "Cache-Control") def SSEResponse( diff --git a/src/types/response.rs b/src/types/response.rs index 389c91fc5..09bc7ffcb 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -315,11 +315,6 @@ impl PyStreamingResponse { headers.set("Content-Type".to_string(), "text/event-stream".to_string()); headers.set("Cache-Control".to_string(), "no-cache".to_string()); headers.set("Connection".to_string(), "keep-alive".to_string()); - headers.set("Access-Control-Allow-Origin".to_string(), "*".to_string()); - headers.set( - "Access-Control-Allow-Headers".to_string(), - "Cache-Control".to_string(), - ); } else { // For non-SSE streaming responses, still set appropriate headers headers.set("Content-Type".to_string(), media_type.clone()); @@ -564,21 +559,6 @@ impl FromPyObject<'_, '_> for StreamingResponse { if headers.get("Connection".to_string()).is_none() { headers.set("Connection".to_string(), "keep-alive".to_string()); } - if headers - .get("Access-Control-Allow-Origin".to_string()) - .is_none() - { - headers.set("Access-Control-Allow-Origin".to_string(), "*".to_string()); - } - if headers - .get("Access-Control-Allow-Headers".to_string()) - .is_none() - { - headers.set( - "Access-Control-Allow-Headers".to_string(), - "Cache-Control".to_string(), - ); - } } let content: pyo3::Py = match obj.getattr("content") { From 4a3cf5a9f853faa359be4a7979716e4ad8879a9e Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:47:28 +0000 Subject: [PATCH 027/106] fix: json serialisation (#1301) --- integration_tests/base_routes.py | 54 +++++++++++++++++++++++ integration_tests/test_json_types.py | 64 +++++++++++++++++++++++++++- robyn/jsonify.py | 10 +++-- robyn/router.py | 4 +- 4 files changed, 125 insertions(+), 7 deletions(-) diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 6cf22a2a1..67891226c 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -376,6 +376,60 @@ async def async_json_get(): return jsonify({"async json get": "json"}) +# JSON List (auto-serialized without explicit jsonify) + + +@app.get("/sync/json/list") +def sync_json_list_get(): + return [ + {"id": 1, "title": "First Post", "published": True}, + {"id": 2, "title": "Draft Post", "published": False}, + {"id": 3, "title": "Latest Post", "published": True}, + ] + + +@app.get("/async/json/list") +async def async_json_list_get(): + return [ + {"id": 1, "title": "First Post", "published": True}, + {"id": 2, "title": "Draft Post", "published": False}, + {"id": 3, "title": "Latest Post", "published": True}, + ] + + +@app.get("/sync/json/list/empty") +def sync_json_list_empty_get(): + return [] + + +@app.get("/async/json/list/empty") +async def async_json_list_empty_get(): + return [] + + +@app.get("/sync/json/list/primitives") +def sync_json_list_primitives_get(): + return [1, 2, 3, "four", True, None] + + +@app.get("/async/json/list/primitives") +async def async_json_list_primitives_get(): + return [1, 2, 3, "four", True, None] + + +# JSON Dict (auto-serialized without explicit jsonify) + + +@app.get("/sync/json/dict") +def sync_json_dict_get(): + return {"message": "sync dict", "count": 42, "active": True} + + +@app.get("/async/json/dict") +async def async_json_dict_get(): + return {"message": "async dict", "count": 42, "active": True} + + @app.get("/sync/json/const", const=True) def sync_json_const_get(): return jsonify({"sync json const get": "json"}) diff --git a/integration_tests/test_json_types.py b/integration_tests/test_json_types.py index ab8d5b9b7..885f414e8 100644 --- a/integration_tests/test_json_types.py +++ b/integration_tests/test_json_types.py @@ -1,6 +1,6 @@ import pytest -from integration_tests.helpers.http_methods_helpers import json_post +from integration_tests.helpers.http_methods_helpers import get, json_post @pytest.mark.parametrize("function_type", ["sync", "async"]) @@ -117,3 +117,65 @@ def test_json_mixed_types_preserved(function_type: str, session): assert result["field_name"]["type"] == "str" assert result["field_value"]["value"] == "111000111" assert result["field_value"]["type"] == "str" + + +# ===== JSON List Serialization Tests (Issue #1300) ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_list_response_serialization(function_type: str, session): + """Test that returning a list from a handler is properly serialized as JSON""" + res = get(f"/{function_type}/json/list") + + # Check content type is application/json + assert res.headers["content-type"] == "application/json" + + # Check that response is valid JSON (not Python str representation) + result = res.json() + assert isinstance(result, list) + assert len(result) == 3 + + # Verify the data structure and types are correct + assert result[0] == {"id": 1, "title": "First Post", "published": True} + assert result[1] == {"id": 2, "title": "Draft Post", "published": False} + assert result[2] == {"id": 3, "title": "Latest Post", "published": True} + + # Verify booleans are proper JSON booleans (true/false), not Python (True/False) + # This is implicitly tested by res.json() succeeding, but let's verify the raw response too + assert "true" in res.text.lower() + assert "false" in res.text.lower() + assert "True" not in res.text # Python boolean should not appear + assert "False" not in res.text + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_empty_list_response_serialization(function_type: str, session): + """Test that returning an empty list is properly serialized as JSON""" + res = get(f"/{function_type}/json/list/empty") + + assert res.headers["content-type"] == "application/json" + result = res.json() + assert result == [] + assert res.text == "[]" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_list_primitives_response_serialization(function_type: str, session): + """Test that a list of primitives is properly serialized as JSON""" + res = get(f"/{function_type}/json/list/primitives") + + assert res.headers["content-type"] == "application/json" + result = res.json() + assert result == [1, 2, 3, "four", True, None] + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_dict_response_auto_serialization(function_type: str, session): + """Test that returning a dict from a handler is properly auto-serialized as JSON""" + res = get(f"/{function_type}/json/dict") + + assert res.headers["content-type"] == "application/json" + result = res.json() + assert result["message"] == f"{function_type} dict" + assert result["count"] == 42 + assert result["active"] is True diff --git a/robyn/jsonify.py b/robyn/jsonify.py index 28b00c90b..915a05832 100644 --- a/robyn/jsonify.py +++ b/robyn/jsonify.py @@ -1,13 +1,15 @@ +from typing import Any, Dict, List, Union + import orjson -def jsonify(input_dict: dict) -> str: +def jsonify(data: Union[Dict[str, Any], List[Any]]) -> str: """ - This function serializes input dict to a json string + This function serializes input data to a json string Attributes: - input_dict dict: response of the function + data: dict or list to serialize as JSON response """ - output_binary = orjson.dumps(input_dict) + output_binary = orjson.dumps(data) output_str = output_binary.decode("utf-8") return output_str diff --git a/robyn/router.py b/robyn/router.py index dff065d32..a8e11bc40 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -72,7 +72,7 @@ def _format_tuple_response(self, res: tuple) -> Response: def _format_response( self, - res: Union[Dict, Response, StreamingResponse, bytes, tuple, str], + res: Union[Dict, List, Response, StreamingResponse, bytes, tuple, str], ) -> Union[Response, StreamingResponse]: if isinstance(res, Response): return res @@ -80,7 +80,7 @@ def _format_response( if isinstance(res, StreamingResponse): return res - if isinstance(res, dict): + if isinstance(res, (dict, list)): return Response( status_code=status_codes.HTTP_200_OK, headers=Headers({"Content-Type": "application/json"}), From a1cbdce1b79242c41b74104efd23d9df4708ef69 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Thu, 5 Feb 2026 01:15:06 +0000 Subject: [PATCH 028/106] fix: docs - advanced routing (#1302) --- .../en/api_reference/advanced_routing.mdx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx b/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx index 2811452f0..9f10c9095 100644 --- a/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx @@ -384,7 +384,7 @@ The parameter injection system works in two phases: - Apply middleware to entire SubRouter groups for common functionality like authentication. + Configure authentication handlers on SubRouters and apply authentication to routes using `auth_required=True`. @@ -399,23 +399,26 @@ The parameter injection system works in two phases: def authenticate(self, request): auth_header = request.headers.get("authorization", "") if not auth_header.startswith("Bearer "): - return False + return None token = auth_header[7:] # Remove "Bearer " return self.validate_admin_token(token) def validate_admin_token(self, token): # Your token validation logic - return token == "admin-secret-token" + if token == "admin-secret-token": + return {"user": "admin"} # Return identity object + return None - # Apply authentication to all admin routes - admin.add_auth_handler(AdminAuth()) + # Configure the authentication handler for this SubRouter + admin.configure_authentication(AdminAuth()) - @admin.get("/users") + # Routes must explicitly require authentication with auth_required=True + @admin.get("/users", auth_required=True) def admin_users(): return {"admin_users": ["user1", "user2"]} - @admin.delete("/users/:id") + @admin.delete("/users/:id", auth_required=True) def delete_user(path_params): return {"deleted": path_params["id"]} ``` From 67abc4c5971514ad4ac82ce5308dcc974c0d26a5 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Fri, 6 Feb 2026 22:52:33 +0000 Subject: [PATCH 029/106] Release 0.77.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- docs_src/public/llms.txt | 2 +- llms.txt | 2 +- pyproject.toml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8a77a6c2..ae7801fd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.76.0" +version = "0.77.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 785b55633..74b912d9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.76.0" +version = "0.77.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/docs_src/public/llms.txt b/docs_src/public/llms.txt index 3a3adcc73..93b40a43d 100644 --- a/docs_src/public/llms.txt +++ b/docs_src/public/llms.txt @@ -4,7 +4,7 @@ ## Quick Facts -- Version: 0.76.0 +- Version: 0.77.0 - Python: >= 3.10 - License: BSD 2.0 - Repository: https://github.com/sparckles/robyn diff --git a/llms.txt b/llms.txt index 3a3adcc73..93b40a43d 100644 --- a/llms.txt +++ b/llms.txt @@ -4,7 +4,7 @@ ## Quick Facts -- Version: 0.76.0 +- Version: 0.77.0 - Python: >= 3.10 - License: BSD 2.0 - Repository: https://github.com/sparckles/robyn diff --git a/pyproject.toml b/pyproject.toml index 61c6edbf9..2ecf38e62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.76.0" +version = "0.77.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -67,7 +67,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.76.0" +version = "0.77.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From ec28a79ed00039979f53a128b30d775c57c1f944 Mon Sep 17 00:00:00 2001 From: Taufik Khan Date: Sun, 8 Feb 2026 07:22:50 +0530 Subject: [PATCH 030/106] fix: correct openapi path parameter generation for nested routes (#1272) * fix: correctly parse nested path parameters in OpenAPI. issue #1270 * fix: correct openapi path parameter generation for nested routes #1270 * docs: improved docstrings and added edge-case tests for #1270 * Apply suggestion from @sansyrox * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- robyn/openapi.py | 21 +++++----- unit_tests/test_openapi_issue_1270.py | 58 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 unit_tests/test_openapi_issue_1270.py diff --git a/robyn/openapi.py b/robyn/openapi.py index 0e0050d76..f3f189068 100644 --- a/robyn/openapi.py +++ b/robyn/openapi.py @@ -1,5 +1,6 @@ import inspect import json +import re import typing from dataclasses import asdict, dataclass, field from importlib import resources @@ -275,27 +276,23 @@ def get_path_obj( # initialized with endpoint for handling endpoints without path params endpoint_with_path_params_wrapped_in_braces = endpoint - endpoint_path_params_split = endpoint.split(":") + path_param_names = re.findall(r":(\w+)", endpoint) - if len(endpoint_path_params_split) > 1: - endpoint_without_path_params = endpoint_path_params_split[0] - - endpoint_with_path_params_wrapped_in_braces = ( - endpoint_without_path_params[:-1] if endpoint_without_path_params.endswith("/") else endpoint_without_path_params - ) - - for path_param in endpoint_path_params_split[1:]: - path_param_name = path_param[:-1] if path_param.endswith("/") else path_param + if path_param_names: + # Convert param syntax to OpenAPI's {param} syntax + # \w+ matches word characters (letters, digits, underscores) and does not match '/', + # so each :param is captured individually without swallowing intervening path segments. + endpoint_with_path_params_wrapped_in_braces = re.sub(r":(\w+)", r"{\1}", endpoint) + for name in path_param_names: openapi_path_object["parameters"].append( { - "name": path_param_name, + "name": name, "in": "path", "required": True, "schema": {"type": "string"}, } ) - endpoint_with_path_params_wrapped_in_braces += "/{" + path_param_name + "}" if query_params: query_param_annotations = query_params.__annotations__ if query_params is str_typed_dict else typing.get_type_hints(query_params) diff --git a/unit_tests/test_openapi_issue_1270.py b/unit_tests/test_openapi_issue_1270.py new file mode 100644 index 000000000..6b97f9636 --- /dev/null +++ b/unit_tests/test_openapi_issue_1270.py @@ -0,0 +1,58 @@ +from robyn.openapi import OpenAPI, OpenAPIInfo + + +def test_openapi_nested_path_parsing(): + """ + Test for Issue #1270: Ensures nested path parameters + like /users/:id/posts/:post_id are parsed correctly. + """ + # Initialize OpenAPI with default info + openapi = OpenAPI(info=OpenAPIInfo()) + + # A dummy handler for the test + def mock_handler(): + pass + + # 1. Test Standard Nested Route + openapi.add_openapi_path_obj( + route_type="get", endpoint="/users/:id/posts/:post_id", openapi_name="get_user_posts", openapi_tags=["testing"], handler=mock_handler + ) + + # 2. Test Parameters with Underscores + openapi.add_openapi_path_obj( + route_type="get", endpoint="/orgs/:org_id/members/:user_id", openapi_name="get_org_member", openapi_tags=["testing"], handler=mock_handler + ) + + # 3. Test Triple Nested Parameters + openapi.add_openapi_path_obj(route_type="get", endpoint="/a/:p1/b/:p2/c/:p3", openapi_name="triple_nested", openapi_tags=["testing"], handler=mock_handler) + + # 4. Test Route Without Parameters (Static) + openapi.add_openapi_path_obj(route_type="get", endpoint="/health", openapi_name="health_check", openapi_tags=["testing"], handler=mock_handler) + + generated_spec = openapi.get_openapi_config() + paths = generated_spec["paths"] + + # Assertions for Case 1: Standard Nested + expected_path_1 = "/users/{id}/posts/{post_id}" + assert expected_path_1 in paths + params_1 = [p["name"] for p in paths[expected_path_1]["get"]["parameters"]] + assert "id" in params_1 + assert "post_id" in params_1 + assert "id/posts" not in params_1 + + # Assertions for Case 2: Underscores + expected_path_2 = "/orgs/{org_id}/members/{user_id}" + assert expected_path_2 in paths + params_2 = [p["name"] for p in paths[expected_path_2]["get"]["parameters"]] + assert "org_id" in params_2 + assert "user_id" in params_2 + + # Assertions for Case 3: Triple Nested + expected_path_3 = "/a/{p1}/b/{p2}/c/{p3}" + assert expected_path_3 in paths + params_3 = [p["name"] for p in paths[expected_path_3]["get"]["parameters"]] + assert len(params_3) == 3 + + # Assertions for Case 4: Static Route + assert "/health" in paths + assert len(paths["/health"]["get"]["parameters"]) == 0 From 68aff1d07f3736c556a36fbac82efba5643b3d9f Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:45:40 +0000 Subject: [PATCH 031/106] docs: remove typed vs untyped distinction (#1305) --- .../en/api_reference/advanced_features.mdx | 12 +- .../en/api_reference/authentication.mdx | 24 +- .../en/api_reference/const_requests.mdx | 14 +- .../documentation/en/api_reference/cors.mdx | 9 +- .../en/api_reference/dependency_injection.mdx | 30 +- .../en/api_reference/exceptions.mdx | 8 +- .../en/api_reference/file-uploads.mdx | 91 +----- .../en/api_reference/form_data.mdx | 9 +- .../en/api_reference/getting_started.mdx | 214 ++------------ .../en/api_reference/middlewares.mdx | 29 +- .../api_reference/multiprocess_execution.mdx | 28 +- .../en/api_reference/openapi.mdx | 125 +-------- .../en/api_reference/redirection.mdx | 2 +- .../en/api_reference/request_object.mdx | 19 +- .../en/api_reference/templating.mdx | 38 +-- .../en/api_reference/using_rust_directly.mdx | 20 +- .../en/api_reference/websockets.mdx | 124 +-------- .../documentation/en/example_app/openapi.mdx | 125 +-------- .../zh/api_reference/advanced_features.mdx | 12 +- .../zh/api_reference/authentication.mdx | 25 +- .../zh/api_reference/const_requests.mdx | 15 +- .../documentation/zh/api_reference/cors.mdx | 9 +- .../zh/api_reference/dependency_injection.mdx | 31 +-- .../zh/api_reference/exceptions.mdx | 8 +- .../zh/api_reference/file-uploads.mdx | 91 +----- .../zh/api_reference/form_data.mdx | 9 +- .../zh/api_reference/getting_started.mdx | 262 ++---------------- .../zh/api_reference/middlewares.mdx | 30 +- .../api_reference/multiprocess_execution.mdx | 28 +- .../zh/api_reference/openapi.mdx | 125 +-------- .../zh/api_reference/redirection.mdx | 2 +- .../zh/api_reference/request_object.mdx | 19 +- .../zh/api_reference/scaling.mdx | 6 +- .../zh/api_reference/templating.mdx | 38 +-- .../zh/api_reference/using_rust_directly.mdx | 20 +- .../documentation/zh/api_reference/views.mdx | 59 +--- .../zh/api_reference/websockets.mdx | 121 +------- .../documentation/zh/example_app/openapi.mdx | 125 +-------- 38 files changed, 130 insertions(+), 1826 deletions(-) diff --git a/docs_src/src/pages/documentation/en/api_reference/advanced_features.mdx b/docs_src/src/pages/documentation/en/api_reference/advanced_features.mdx index 26de23d44..e73101a5d 100644 --- a/docs_src/src/pages/documentation/en/api_reference/advanced_features.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/advanced_features.mdx @@ -14,17 +14,7 @@ Batman scaled his application across multiple cores for better performance. He u - ```python {{ title: 'untyped' }} - from robyn import Robyn - - app = Robyn(__file__) - - @app.get("/") - async def h(request): - return f"hello to you, {request.ip_addr}" - - ``` - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, Request app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/en/api_reference/authentication.mdx b/docs_src/src/pages/documentation/en/api_reference/authentication.mdx index 61a10e568..1b91f890c 100644 --- a/docs_src/src/pages/documentation/en/api_reference/authentication.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/authentication.mdx @@ -19,15 +19,7 @@ As Batman found out, Robyn provides an easy way to add an authentication middlew - ```python {{ title: 'untyped' }} - @app.get("/auth", auth_required=True) - async def auth(request: Request): - # This route method will only be executed if the user is authenticated - # Otherwise, a 401 response will be returned - return "Hello, world" - ``` - - ```python {{title: 'typed'}} + ```python @app.get("/auth", auth_required=True) async def auth(request: Request): # This route method will only be executed if the user is authenticated @@ -48,19 +40,7 @@ As Batman found out, Robyn provides an easy way to add an authentication middlew - ```python {{ title: 'untyped' }} - class BasicAuthHandler(AuthenticationHandler): - def authenticate(self, request: Request) -> Optional[Identity]: - token = self.token_getter.get_token(request) - if token == "valid": - return Identity(claims={}) - return None - - app.configure_authentication(BasicAuthHandler(token_getter=BearerGetter())) - - ``` - - ```python {{title: 'typed'}} + ```python class BasicAuthHandler(AuthenticationHandler): def authenticate(self, request: Request) -> Optional[Identity]: token = self.token_getter.get_token(request) diff --git a/docs_src/src/pages/documentation/en/api_reference/const_requests.mdx b/docs_src/src/pages/documentation/en/api_reference/const_requests.mdx index 95b9bec84..184020582 100644 --- a/docs_src/src/pages/documentation/en/api_reference/const_requests.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/const_requests.mdx @@ -18,13 +18,7 @@ Robyn told Batman that you can pre-compute the response for each route. This wil - ```python {{ title: 'untyped' }} - @app.get("/", const=True) - async def h(): - return "Hello, world" - ``` - - ```python {{title: 'typed'}} + ```python @app.get("/", const=True) async def h(): return "Hello, world" @@ -47,11 +41,7 @@ Robyn told Batman that he can use the `--workers` flag to scale the application - ```python {{ title: 'untyped' }} - python3 app.py --workers=N --process=M - ``` - - ```python {{title: 'typed'}} + ```python python3 app.py --workers=N --process=M ``` diff --git a/docs_src/src/pages/documentation/en/api_reference/cors.mdx b/docs_src/src/pages/documentation/en/api_reference/cors.mdx index ba3083512..094da3c2c 100644 --- a/docs_src/src/pages/documentation/en/api_reference/cors.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/cors.mdx @@ -17,14 +17,7 @@ You can allow CORS for your application by adding the following code: - ```python {{ title: 'untyped' }} - from robyn import Robyn, ALLOW_CORS - - app = Robyn(__file__) - ALLOW_CORS(app, origins = ["http://localhost:/"]) - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, ALLOW_CORS app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx b/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx index 3490507c5..61859fb8b 100644 --- a/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx @@ -21,20 +21,7 @@ Application level dependency injection is used to inject dependencies into the a - ```python {{ title: 'untyped' }} - from robyn import Robyn, ALLOW_CORS - - app = Robyn(__file__) - GLOBAL_DEPENDENCY = "GLOBAL DEPENDENCY" - - app.inject_global(GLOBAL_DEPENDENCY=GLOBAL_DEPENDENCY) - - @app.get("/sync/global_di") - def sync_global_di(request, global_dependencies): - return global_dependencies["GLOBAL_DEPENDENCY"] - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, ALLOW_CORS app = Robyn(__file__) @@ -62,20 +49,7 @@ Router level dependency injection is used to inject dependencies into the router - ```python {{ title: 'untyped' }} - from robyn import Robyn, ALLOW_CORS - - app = Robyn(__file__) - ROUTER_DEPENDENCY = "ROUTER DEPENDENCY" - - app.inject(ROUTER_DEPENDENCY=ROUTER_DEPENDENCY) - - @app.get("/sync/global_di") - def sync_global_di(r, router_dependencies): # r is the request object - return router_dependencies["ROUTER_DEPENDENCY"] - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, ALLOW_CORS, Request app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/en/api_reference/exceptions.mdx b/docs_src/src/pages/documentation/en/api_reference/exceptions.mdx index fb0b2d921..1f0fabc43 100644 --- a/docs_src/src/pages/documentation/en/api_reference/exceptions.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/exceptions.mdx @@ -9,13 +9,7 @@ Batman learned how to create custom error handlers for different exception types - ```python {{ title: 'untyped' }} - @app.exception - def handle_exception(error): - return Response(status_code=500, description=f"error msg: {error}", headers={}) - ``` - - ```python {{ title: 'typed' }} + ```python @app.exception def handle_exception(error: Exception): return Response(status_code=500, description=f"error msg: {error}", headers={}) diff --git a/docs_src/src/pages/documentation/en/api_reference/file-uploads.mdx b/docs_src/src/pages/documentation/en/api_reference/file-uploads.mdx index 728cea71a..a69df1579 100644 --- a/docs_src/src/pages/documentation/en/api_reference/file-uploads.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/file-uploads.mdx @@ -16,19 +16,7 @@ Batman scaled his application across multiple cores for better performance. He u - ```python {{ title: 'untyped' }} - @app.post("/upload") - async def upload(): - body = request.body - file = bytearray(body) - - # write whatever filename - with open('test.txt', 'wb') as f: - f.write(body) - - return {'message': 'success'} - ``` - ```python {{ title: 'typed' }} + ```python @app.post("/upload") async def upload(): body = request.body @@ -56,15 +44,7 @@ Batman scaled his application across multiple cores for better performance. He u - ```python {{ title: 'untyped' }} - - @app.post("/sync/multipart-file") - def sync_multipart_file(request: Request): - files = request.files - file_names = files.keys() - return {"file_names": list(file_names)} - ``` - ```python {{ title: 'typed' }} + ```python @app.post("/sync/multipart-file") def sync_multipart_file(request: Request): @@ -94,21 +74,7 @@ Batman scaled his application across multiple cores for better performance. He u - ```python {{ title: 'untyped' }} - from robyn import Robyn, serve_html - - app = Robyn(__file__) - - - @app.get("/") - async def h(request): - return serve_html("./index.html") - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, Request, serve_html app = Robyn(__file__) @@ -136,22 +102,7 @@ Speaking of HTML files, Batman wanted to serve simple HTML strings. He was sugge - ```python {{ title: 'untyped' }} - from robyn import Robyn, html - - app = Robyn(__file__) - - - @app.get("/") - async def h(request): - html_string = "

Hello World

" - return html(html_string) - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, Request, html app = Robyn(__file__) @@ -181,21 +132,7 @@ Now, that Batman was able to serve HTML files, he wanted to serve other files li - ```python {{ title: 'untyped' }} - from robyn import Robyn, serve_file - - app = Robyn(__file__) - - - @app.get("/") - async def h(request): - return serve_file("./index.html", file_name="index.html") # file_name is optional - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, serve_file, Request app = Robyn(__file__) @@ -224,23 +161,7 @@ After serving other files, Batman wanted to serve directories, e.g. to serve a R - ```python {{ title: 'untyped' }} - from robyn import Robyn, serve_file - - app = Robyn(__file__) - - - app.serve_directory( - route="/test_dir", - directory_path=os.path.join(current_file_path, "build"), - index_file="index.html", - ) - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, serve_file, Request app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/en/api_reference/form_data.mdx b/docs_src/src/pages/documentation/en/api_reference/form_data.mdx index 99c57727b..8c0fabc7c 100644 --- a/docs_src/src/pages/documentation/en/api_reference/form_data.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/form_data.mdx @@ -16,14 +16,7 @@ Batman uploaded some multipart form data and wanted to handle it using the follo - ```python {{ title: 'untyped' }} - @app.post("/upload") - async def upload(request): - form_data = request.form_data - - return form_data - ``` - ```python {{ title: 'typed' }} + ```python @app.post("/upload") async def upload(request: Request): form_data = request.form_data diff --git a/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx b/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx index 91585f505..0cf3abe78 100644 --- a/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx @@ -30,19 +30,7 @@ Robyn supports both synchronous and asynchronous request handlers, allowing you - ```python {{ title: 'untyped' }} - from robyn import Robyn - - app = Robyn(__file__) - - @app.get("/") - def h(request): - return "Hello, world" - - app.start(port=8080, host="0.0.0.0") # host is optional, defaults to 127.0.0.1 - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Robyn, Request app = Robyn(__file__) @@ -51,10 +39,7 @@ Robyn supports both synchronous and asynchronous request handlers, allowing you def h(request: Request): return "Hello, world" - app.start(port=8080, host="0.0.0.0") - - - + app.start(port=8080, host="0.0.0.0") # host is optional, defaults to 127.0.0.1 ``` @@ -67,14 +52,7 @@ Robyn supports both synchronous and asynchronous request handlers, allowing you - ```python {{ title: 'untyped' }} - @app.get("/") - async def h(request): - return "Hello, world" - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -756,19 +734,7 @@ Batman learned to customize response formats by returning dictionaries or using - ```python {{ title: 'untyped' }} - @app.post("/dictionary") - async def dictionary(request): - return { - "status_code": 200, - "description": "This is a regular response", - "type": "text", - "headers": {"Header": "header_value"}, - } - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.post("/dictionary") @@ -780,7 +746,6 @@ Batman learned to customize response formats by returning dictionaries or using "headers": {"Header": "header_value"}, } - ``` @@ -796,15 +761,7 @@ To use the Response object, he wrote: - ```python {{ title: 'untyped' }} - from robyn.robyn import Response - - @app.get("/response") - async def response(request): - return Response(status_code=200, headers=Headers({}), description="OK") - ``` - - ```python {{title: 'typed'}} + ```python from robyn.robyn import Response, Request @app.get("/response") @@ -826,31 +783,7 @@ Batman then wanted to return a binary output from his application. He could do t - ```python {{ title: 'untyped' }} - @app.get("/binary_output_response_sync") - def binary_output_response_sync(request): - return Response( - status_code=200, - headers={"Content-Type": "application/octet-stream"}, - description="OK", - ) - - - @app.get("/binary_output_async") - async def binary_output_async(request): - return b"OK" - - - @app.get("/binary_output_response_async") - async def binary_output_response_async(request): - return Response( - status_code=200, - headers={"Content-Type": "application/octet-stream"}, - description="OK", - ) - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request, Response @app.get("/binary_output_response_sync") @@ -898,17 +831,7 @@ Either, by using the `headers` field in the `Response` object: - ```python {{ title: 'untyped' }} - @app.get("/") - def binary_output_response_sync(request): - return Response( - status_code=200, - headers={"Content-Type": "application/octet-stream"}, - description="OK", - ) - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -933,11 +856,7 @@ Either, by using the `headers` field in the `Response` object: - ```python {{ title: 'untyped' }} - app.add_response_header("content-type", "application/json") - ``` - - ```python {{title: 'typed'}} + ```python app.add_response_header("content-type", "application/json") ``` @@ -951,11 +870,7 @@ Either, by using the `headers` field in the `Response` object: - ```python {{ title: 'untyped' }} - app.set_response_header("content-type", "application/json") - ``` - - ```python {{title: 'typed'}} + ```python app.set_response_header("content-type", "application/json") ``` @@ -968,11 +883,7 @@ To prevent the headers from getting applied to certain endpoints, you can use th - ```python {{ title: 'untyped' }} - app.exclude_response_headers_for(["/login", "/signup"]) - ``` - - ```python {{title: 'typed'}} + ```python app.exclude_response_headers_for(["/login", "/signup"]) ``` @@ -990,17 +901,7 @@ Robyn provides a complete cookie API following RFC 6265. Set cookies using the ` - ```python {{ title: 'untyped' }} - from robyn import Response, Headers - - @app.get("/") - def set_session(request): - response = Response(200, Headers({}), "Welcome!") - response.set_cookie(key="session", value="abc123") - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/") @@ -1030,23 +931,7 @@ You can set additional cookie attributes for security and control: - ```python {{ title: 'untyped' }} - @app.get("/login") - def login(request): - response = Response(200, Headers({}), "Logged in") - response.set_cookie( - key="auth_token", - value="secret123", - path="/", - max_age=3600, # 1 hour - secure=True, # HTTPS only - http_only=True, # No JavaScript access - same_site="Strict", # CSRF protection - ) - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/login") @@ -1077,15 +962,7 @@ To delete a cookie from the browser, use the `delete` method on the cookies coll - ```python {{ title: 'untyped' }} - @app.get("/logout") - def logout(request): - response = Response(200, Headers({}), "Logged out") - response.cookies.delete("auth_token") - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/logout") @@ -1108,28 +985,7 @@ You can iterate over cookies or access them by name: - ```python {{ title: 'untyped' }} - @app.get("/debug") - def debug_cookies(request): - response = Response(200, Headers({}), "Cookies set") - response.set_cookie("a", "1") - response.set_cookie("b", "2") - - # Get all cookie names - names = response.cookies.keys() - - # Iterate over cookies - for name in response.cookies: - print(f"Cookie: {name}") - - # Check if cookie exists - if "a" in response.cookies: - print("Cookie 'a' exists") - - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/debug") @@ -1171,25 +1027,7 @@ Either, by using the `headers` field in the `Request` object: - ```python {{ title: 'untyped' }} - @app.get("/") - def binary_output_response_sync(request): - headers = request.headers - - print("These are the request headers: ", headers) - existing_header = headers.get("exisiting_header") - existing_header = headers.get("exisiting_header", "default_value") - exisiting_header = headers["exisiting_header"] # This syntax is also valid - - headers.set("modified", "modified_value") - headers["new_header"] = "new_value" # This syntax is also valid - - print("These are the modified request headers: ", headers) - - return "" - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -1222,11 +1060,7 @@ Or by using the global Request Headers: - ```python {{ title: 'untyped' }} - app.add_request_header("server", "robyn") - ``` - - ```python {{title: 'typed'}} + ```python app.add_request_header("server", "robyn") ``` @@ -1242,11 +1076,7 @@ Or by using the global Request Headers: - ```python {{ title: 'untyped' }} - app.set_request_header("server", "robyn") - ``` - - ```python {{title: 'typed'}} + ```python app.set_request_header("server", "robyn") ``` @@ -1266,15 +1096,7 @@ After learning about response formats and headers, Batman learned to set status - ```python {{ title: 'untyped' }} - from robyn import status_codes - - @app.get("/response") - async def response(request): - return Response(status_code=status_codes.HTTP_200_OK, headers=Headers({}), description="OK") - ``` - - ```python {{title: 'typed'}} + ```python from robyn import status_codes, Request diff --git a/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx b/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx index efd09fce5..5a9427edf 100644 --- a/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/middlewares.mdx @@ -18,16 +18,12 @@ Batman was excited to learn that he could add events as functions as well as dec - ```python {{ title: 'untyped' }} + ```python async def startup_handler(): print("Starting up") app.startup_handler(startup_handler) - - ``` - - ```python {{title: 'typed'}} @app.shutdown_handler def shutdown_handler(): print("Shutting down") @@ -45,14 +41,7 @@ Batman was excited to learn that he could add events as functions as well as dec - ```python {{ title: 'untyped' }} - @app.get("/") - async def h(request): - return "Hello, world" - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -88,19 +77,7 @@ Batman was excited to learn that he could add events as functions as well as dec - ```python {{ title: 'untyped' }} - @app.before_request("/") - async def hello_before_request(request: Request): - request.headers["before"] = "sync_before_request" - return request - - @app.after_request("/") - def hello_after_request(response: Response): - response.headers.set("after", "sync_after_request") - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response @app.before_request("/") diff --git a/docs_src/src/pages/documentation/en/api_reference/multiprocess_execution.mdx b/docs_src/src/pages/documentation/en/api_reference/multiprocess_execution.mdx index 598d31dda..b3de78b11 100644 --- a/docs_src/src/pages/documentation/en/api_reference/multiprocess_execution.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/multiprocess_execution.mdx @@ -21,33 +21,7 @@ If one needs a variable to be protected within a process, while accessing it fro - ```python {{ title: 'untyped' }} - import threading - import time - from multiprocessing import Value - - from robyn import Robyn, Request - - app = Robyn(__file__) - - count = Value("i", 0) - - def counter(): - while True: - count.value += 1 - time.sleep(0.2) - print(count.value, "added 1") - - @app.get("/") - def index(request): - return f"{count.value}" - - threading.Thread(target=counter, daemon=True).start() - - app.start() - ``` - - ```python {{ title: 'typed' }} + ```python import threading import time from multiprocessing import Value diff --git a/docs_src/src/pages/documentation/en/api_reference/openapi.mdx b/docs_src/src/pages/documentation/en/api_reference/openapi.mdx index 8523b15e2..d82e3e935 100644 --- a/docs_src/src/pages/documentation/en/api_reference/openapi.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/openapi.mdx @@ -29,62 +29,7 @@ python app.py --disable-openapi -```python {{ title: 'untyped' }} -from robyn import Robyn -from robyn.robyn import QueryParams - -app = Robyn( - file_object=__file__, - openapi=OpenAPI( - info=OpenAPIInfo( - title="Sample App", - description="This is a sample server application.", - termsOfService="https://example.com/terms/", - version="1.0.0", - contact=Contact( - name="API Support", - url="https://www.example.com/support", - email="support@example.com", - ), - license=License( - name="BSD2.0", - url="https://opensource.org/license/bsd-2-clause", - ), - externalDocs=ExternalDocumentation(description="Find more info here", url="https://example.com/"), - components=Components(), - ), - ), -) - - -@app.get("/") -async def welcome(): - """welcome endpoint""" - return "hi" - - -class GetRequestParams(QueryParams): - appointment_id: str - year: int - - -@app.get("/api/v1/name", openapi_name="Name Route", openapi_tags=["Name"]) -async def get(r, query_params: GetRequestParams): - """Get Name by ID""" - return r.query_params - - -@app.delete("/users/:name", openapi_tags=["Name"]) -async def delete(r): - """Delete Name by ID""" - return r.path_params - - -if __name__ == "__main__": - app.start() -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Robyn, Request @@ -146,40 +91,7 @@ if __name__ == "__main__": -```python {{ title: 'untyped' }} -from robyn import SubRouter -from robyn.robyn import QueryParams - -subrouter = SubRouter(__name__, prefix="/sub") - - -@subrouter.get("/") -async def subrouter_welcome(): - """welcome subrouter""" - return "hiiiiii subrouter" - - -class SubRouterGetRequestParams(QueryParams): - _id: int - value: str - - -@subrouter.get("/name") -async def subrouter_get(r, query_params: SubRouterGetRequestParams): - """Get Name by ID""" - return r.query_params - - -@subrouter.delete("/:name") -async def subrouter_delete(r): - """Delete Name by ID""" - return r.path_params - - -app.include_router(subrouter) -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Request, SubRouter @@ -221,38 +133,7 @@ We support all the params mentioned in the latest OpenAPI specifications (https: -```python {{ title: 'untyped' }} -from robyn.types import JSONResponse, Body - -class Initial(Body): - is_present: bool - letter: Optional[str] - - -class FullName(Body): - first: str - second: str - initial: Initial - - -class CreateItemBody(Body): - name: FullName - description: str - price: float - tax: float - - -class CreateResponse(JSONResponse): - success: bool - items_changed: int - - -@app.post("/") -def create_item(request: Request, body: CreateItemBody) -> CreateResponse: - return CreateResponse(success=True, items_changed=2) -``` - -```python {{ title: 'typed' }} +```python from robyn.types import JSONResponse, Body class Initial(Body): diff --git a/docs_src/src/pages/documentation/en/api_reference/redirection.mdx b/docs_src/src/pages/documentation/en/api_reference/redirection.mdx index 7aa7350b9..49aef9971 100644 --- a/docs_src/src/pages/documentation/en/api_reference/redirection.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/redirection.mdx @@ -8,7 +8,7 @@ Batman wanted to redirect some endpoints to others. Robyn helped him do so by th - ```python {{title: 'untyped'}} + ```python from robyn import Robyn, Response app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/en/api_reference/request_object.mdx b/docs_src/src/pages/documentation/en/api_reference/request_object.mdx index deb216d51..be01c32cc 100644 --- a/docs_src/src/pages/documentation/en/api_reference/request_object.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/request_object.mdx @@ -50,24 +50,7 @@ identity (Optional[Identity]): The identity of the client - ```python {{ title: 'untyped' }} - @dataclass - class Request: - """ - query_params: QueryParams - headers: Headers - path_params: dict[str, str] - body: Union[str, bytes] - method: str - url: Url - form_data: dict[str, str] - files: dict[str, bytes] - ip_addr: Optional[str] - identity: Optional[Identity] - """ - ``` - - ```python {{ title: 'typed' }} + ```python @dataclass class Request: """ diff --git a/docs_src/src/pages/documentation/en/api_reference/templating.mdx b/docs_src/src/pages/documentation/en/api_reference/templating.mdx index 04764de97..13a74a792 100644 --- a/docs_src/src/pages/documentation/en/api_reference/templating.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/templating.mdx @@ -16,22 +16,7 @@ Batman was excited to learn that he could add events as functions as well as dec - ```python {{title: 'untyped'}} - from robyn.templating import JinjaTemplate - - current_file_path = pathlib.Path(__file__).parent.resolve() - JINJA_TEMPLATE = JinjaTemplate(os.path.join(current_file_path, "templates")) - - @app.get("/template_render") - def template_render(): - context = {"framework": "Robyn", "templating_engine": "Jinja2"} - - template = JINJA_TEMPLATE.render_template(template_name="test.html", **context) - return template - - ``` - - ```python {{title: 'typed'}} + ```python from robyn.templating import JinjaTemplate current_file_path = pathlib.Path(__file__).parent.resolve() @@ -84,11 +69,7 @@ To do that, you need to import the `TemplateInterface` from `robyn.templating` - ```python {{ title: 'untyped' }} - from robyn.templating import TemplateInterface - ``` - - ```python {{ title: 'typed' }} + ```python from robyn.templating import TemplateInterface ``` @@ -103,20 +84,7 @@ Then You need to have a `render_template` method inside your implementation. So, - ```python {{ title: 'untyped' }} - class JinjaTemplate(TemplateInterface): - def __init__(self, directory, encoding="utf-8", followlinks=False): - self.env = Environment( - loader=FileSystemLoader( - searchpath=directory, encoding=encoding, followlinks=followlinks - ) - ) - - def render_template(self, template_name, **kwargs): - return self.env.get_template(template_name).render(**kwargs) - ``` - - ```python {{ title: 'typed' }} + ```python class JinjaTemplate(TemplateInterface): def __init__(self, directory, encoding="utf-8", followlinks=False): self.env = Environment( diff --git a/docs_src/src/pages/documentation/en/api_reference/using_rust_directly.mdx b/docs_src/src/pages/documentation/en/api_reference/using_rust_directly.mdx index 2b1ed32f5..10b2952c3 100644 --- a/docs_src/src/pages/documentation/en/api_reference/using_rust_directly.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/using_rust_directly.mdx @@ -14,11 +14,7 @@ The first thing you need to is to create a Rust file. Let's call it `hello_world - ```python {{ title: 'untyped' }} - python -m robyn --create-rust-file hello_world - ``` - - ```python {{title: 'typed'}} + ```python python -m robyn --create-rust-file hello_world ``` @@ -93,13 +89,7 @@ The first thing you need to is to create a Rust file. Let's call it `hello_world - ```python {{ title: 'untyped' }} - from hello_world import square - - print(square(5)) - ``` - - ```python {{title: 'typed'}} + ```python from hello_world import square print(square(5)) @@ -112,11 +102,7 @@ The first thing you need to is to create a Rust file. Let's call it `hello_world - ```python {{ title: 'untyped' }} - python -m robyn --compile-rust-path "." --dev - ``` - - ```python {{title: 'typed'}} + ```python python -m robyn --compile-rust-path "." --dev ``` diff --git a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx index 4f52101b0..6e508860f 100644 --- a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx @@ -16,7 +16,7 @@ To handle real-time bidirectional communication, Batman learned how to work with - ```python {{ title: 'untyped' }} + ```python from robyn import Robyn, jsonify, WebSocket app = Robyn(__file__) @@ -34,28 +34,6 @@ To handle real-time bidirectional communication, Batman learned how to work with def message(): return "Connected to ws" - - ``` - - ```python {{title: 'typed'}} - from robyn import Robyn, jsonify, WebSocket - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("message") - def connect(): - return "Hello world, from ws" - - @websocket.on("close") - def close(): - return "Goodbye world, from ws" - - @websocket.on("connect") - def message(): - return "Connected to ws" - - ``` @@ -76,31 +54,7 @@ To handle real-time bidirectional communication, Batman learned how to work with - ```python {{ title: 'untyped' }} - from robyn import Robyn, WebSocket - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("connect") - async def connect(): - # No return needed - just log the connection - print("Client connected") - - @websocket.on("message") - def message(ws, msg): - # Process message without responding - process_analytics(msg) - # No return statement needed - - @websocket.on("close") - async def close(): - # Explicitly return None - no message sent - cleanup_resources() - return None - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Robyn, WebSocket, WebSocketConnector app = Robyn(__file__) @@ -135,22 +89,11 @@ To handle real-time bidirectional communication, Batman learned how to work with - ```python {{ title: 'untyped' }} - - @websocket.on("message") - def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - ws.sync_send_to(websocket_id, "This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: websocket_id = ws.id - state = websocket_state[websocket_id] ws.sync_send_to(websocket_id, "This is a message to self") return "" @@ -168,22 +111,11 @@ To handle real-time bidirectional communication, Batman learned how to work with - ```python {{ title: 'untyped' }} - - @websocket.on("message") - async def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - await ws.async_send_to(websocket_id, "This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: websocket_id = ws.id - state = websocket_state[websocket_id] await ws.async_send_to(websocket_id, "This is a message to self") return "" @@ -200,17 +132,7 @@ To handle real-time bidirectional communication, Batman learned how to work with - ```python {{ title: 'untyped' }} - - @websocket.on("message") - def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - ws.sync_broadcast("This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -230,17 +152,7 @@ To handle real-time bidirectional communication, Batman learned how to work with - ```python {{ title: 'untyped' }} - - @websocket.on("message") - async def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - await ws.async_broadcast("This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -260,18 +172,7 @@ To handle real-time bidirectional communication, Batman learned how to work with - ```python {{ title: 'untyped' }} - - @websocket.on("message") - async def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - if (ws.query_params.get("name") == "gordon" and ws.query_params.get("desg") == "commissioner"): - ws.sync_broadcast("Gordon authorized to login!") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -301,16 +202,7 @@ This method is useful for scenarios where you need to programmatically end a Web - ```python {{ title: 'untyped' }} - @websocket.on("message") - def message(ws, msg): - if msg == "disconnect": - ws.close() - return "Closing connection" - return "Message received" - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") def message(ws: WebSocketConnector, msg: str) -> str: if msg == "disconnect": diff --git a/docs_src/src/pages/documentation/en/example_app/openapi.mdx b/docs_src/src/pages/documentation/en/example_app/openapi.mdx index 243327f48..c2b42eefb 100644 --- a/docs_src/src/pages/documentation/en/example_app/openapi.mdx +++ b/docs_src/src/pages/documentation/en/example_app/openapi.mdx @@ -29,62 +29,7 @@ python app.py --disable-openapi -```python {{ title: 'untyped' }} -from robyn import Robyn -from robyn.robyn import QueryParams - -app = Robyn( - file_object=__file__, - openapi=OpenAPI( - info=OpenAPIInfo( - title="Sample App", - description="This is a sample server application.", - termsOfService="https://example.com/terms/", - version="1.0.0", - contact=Contact( - name="API Support", - url="https://www.example.com/support", - email="support@example.com", - ), - license=License( - name="BSD2.0", - url="https://opensource.org/license/bsd-2-clause", - ), - externalDocs=ExternalDocumentation(description="Find more info here", url="https://example.com/"), - components=Components(), - ), - ), -) - - -@app.get("/") -async def welcome(): - """welcome endpoint""" - return "hi" - - -class GetRequestParams(QueryParams): - appointment_id: str - year: int - - -@app.get("/api/v1/name", openapi_name="Name Route", openapi_tags=["Name"]) -async def get(r, query_params: GetRequestParams): - """Get Name by ID""" - return r.query_params - - -@app.delete("/users/:name", openapi_tags=["Name"]) -async def delete(r): - """Delete Name by ID""" - return r.path_params - - -if __name__ == "__main__": - app.start() -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Robyn, Request @@ -146,40 +91,7 @@ if __name__ == "__main__": -```python {{ title: 'untyped' }} -from robyn import SubRouter -from robyn.robyn import QueryParams - -subrouter = SubRouter(__name__, prefix="/sub") - - -@subrouter.get("/") -async def subrouter_welcome(): - """welcome subrouter""" - return "hiiiiii subrouter" - - -class SubRouterGetRequestParams(QueryParams): - _id: int - value: str - - -@subrouter.get("/name") -async def subrouter_get(r, query_params: SubRouterGetRequestParams): - """Get Name by ID""" - return r.query_params - - -@subrouter.delete("/:name") -async def subrouter_delete(r): - """Delete Name by ID""" - return r.path_params - - -app.include_router(subrouter) -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Request, SubRouter @@ -221,38 +133,7 @@ We support all the params mentioned in the latest OpenAPI specifications (https: -```python {{ title: 'untyped' }} -from robyn.types import JSONResponse, Body - -class Initial(Body): - is_present: bool - letter: Optional[str] - - -class FullName(Body): - first: str - second: str - initial: Initial - - -class CreateItemBody(Body): - name: FullName - description: str - price: float - tax: float - - -class CreateResponse(JSONResponse): - success: bool - items_changed: int - - -@app.post("/") -def create_item(request: Request, body: CreateItemBody) -> CreateResponse: - return CreateResponse(success=True, items_changed=2) -``` - -```python {{ title: 'typed' }} +```python from robyn.types import JSONResponse, Body class Initial(Body): diff --git a/docs_src/src/pages/documentation/zh/api_reference/advanced_features.mdx b/docs_src/src/pages/documentation/zh/api_reference/advanced_features.mdx index 60f0c1616..080189e84 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/advanced_features.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/advanced_features.mdx @@ -13,17 +13,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn - - app = Robyn(__file__) - - @app.get("/") - async def h(request): - return f"hello to you, {request.ip_addr}" - - ``` - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, Request app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/zh/api_reference/authentication.mdx b/docs_src/src/pages/documentation/zh/api_reference/authentication.mdx index d620f4f09..425c3c137 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/authentication.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/authentication.mdx @@ -14,22 +14,13 @@ export const description = - ```python {{ title: 'untyped' }} + ```python @app.get("/auth", auth_required=True) async def auth(request: Request): # This route method will only be executed if the user is authenticated # Otherwise, a 401 response will be returned return "Hello, world" ``` - - ```python {{title: 'typed'}} - @app.get("/auth", auth_required=True) - async def auth(request: Request): - # This route method will only be executed if the user is authenticated - # Otherwise, a 401 response will be returned - return "Hello, world" - - ``` @@ -43,19 +34,7 @@ export const description = - ```python {{ title: 'untyped' }} - class BasicAuthHandler(AuthenticationHandler): - def authenticate(self, request: Request) -> Optional[Identity]: - token = self.token_getter.get_token(request) - if token == "valid": - return Identity(claims={}) - return None - - app.configure_authentication(BasicAuthHandler(token_getter=BearerGetter())) - - ``` - - ```python {{title: 'typed'}} + ```python class BasicAuthHandler(AuthenticationHandler): def authenticate(self, request: Request) -> Optional[Identity]: token = self.token_getter.get_token(request) diff --git a/docs_src/src/pages/documentation/zh/api_reference/const_requests.mdx b/docs_src/src/pages/documentation/zh/api_reference/const_requests.mdx index b362ffad7..1a36dfca7 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/const_requests.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/const_requests.mdx @@ -14,18 +14,11 @@ Robyn 告诉蝙蝠侠,可以为每个路由预处理响应,这样即使在 - ```python {{ title: 'untyped' }} + ```python @app.get("/", const=True) async def h(): return "Hello, world" ``` - - ```python {{title: 'typed'}} - @app.get("/", const=True) - async def h(): - return "Hello, world" - - ``` @@ -43,11 +36,7 @@ Robyn 告诉蝙蝠侠,可以使用 `--workers` 参数将应用程序扩展到 - ```python {{ title: 'untyped' }} - python3 app.py --workers=N --process=M - ``` - - ```python {{title: 'typed'}} + ```python python3 app.py --workers=N --process=M ``` diff --git a/docs_src/src/pages/documentation/zh/api_reference/cors.mdx b/docs_src/src/pages/documentation/zh/api_reference/cors.mdx index fbc928d2b..0cc0c9571 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/cors.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/cors.mdx @@ -15,14 +15,7 @@ You can allow CORS for your application by adding the following code: - ```python {{ title: 'untyped' }} - from robyn import Robyn, ALLOW_CORS - - app = Robyn(__file__) - ALLOW_CORS(app, origins = ["http://localhost:/"]) - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, ALLOW_CORS app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx b/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx index d217faeac..55c16cc18 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx @@ -17,7 +17,7 @@ Robyn 提供了两种依赖注入方式: - ```python {{ title: 'untyped' }} + ```python from robyn import Robyn, ALLOW_CORS app = Robyn(__file__) @@ -29,20 +29,6 @@ Robyn 提供了两种依赖注入方式: def sync_global_di(request, global_dependencies): return global_dependencies["GLOBAL_DEPENDENCY"] ``` - - ```python {{ title: 'typed' }} - from robyn import Robyn, ALLOW_CORS - - app = Robyn(__file__) - GLOBAL_DEPENDENCY = "GLOBAL DEPENDENCY" - - app.inject_global(GLOBAL_DEPENDENCY=GLOBAL_DEPENDENCY) - - @app.get("/sync/global_di") - def sync_global_di(request, global_dependencies): - return global_dependencies["GLOBAL_DEPENDENCY"] - - ``` @@ -58,20 +44,7 @@ Robyn 提供了两种依赖注入方式: - ```python {{ title: 'untyped' }} - from robyn import Robyn, ALLOW_CORS - - app = Robyn(__file__) - ROUTER_DEPENDENCY = "ROUTER DEPENDENCY" - - app.inject(ROUTER_DEPENDENCY=ROUTER_DEPENDENCY) - - @app.get("/sync/global_di") - def sync_global_di(r, router_dependencies): # r is the request object - return router_dependencies["ROUTER_DEPENDENCY"] - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, ALLOW_CORS, Request app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/zh/api_reference/exceptions.mdx b/docs_src/src/pages/documentation/zh/api_reference/exceptions.mdx index 0010be160..f3eb99925 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/exceptions.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/exceptions.mdx @@ -9,13 +9,7 @@ - ```python {{ title: 'untyped' }} - @app.exception - def handle_exception(error): - return Response(status_code=500, description=f"error msg: {error}", headers={}) - ``` - - ```python {{ title: 'typed' }} + ```python @app.exception def handle_exception(error: Exception): return Response(status_code=500, description=f"error msg: {error}", headers={}) diff --git a/docs_src/src/pages/documentation/zh/api_reference/file-uploads.mdx b/docs_src/src/pages/documentation/zh/api_reference/file-uploads.mdx index 5ddb35f42..2c64084cc 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/file-uploads.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/file-uploads.mdx @@ -15,19 +15,7 @@ export const description = - ```python {{ title: 'untyped' }} - @app.post("/upload") - async def upload(): - body = request.body - file = bytearray(body) - - # write whatever filename - with open('test.txt', 'wb') as f: - f.write(body) - - return {'message': 'success'} - ``` - ```python {{ title: 'typed' }} + ```python @app.post("/upload") async def upload(): body = request.body @@ -55,15 +43,7 @@ export const description = - ```python {{ title: 'untyped' }} - - @app.post("/sync/multipart-file") - def sync_multipart_file(request: Request): - files = request.files - file_names = files.keys() - return {"file_names": list(file_names)} - ``` - ```python {{ title: 'typed' }} + ```python @app.post("/sync/multipart-file") def sync_multipart_file(request: Request): @@ -93,21 +73,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn, serve_html - - app = Robyn(__file__) - - - @app.get("/") - async def h(request): - return serve_html("./index.html") - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, Request, serve_html app = Robyn(__file__) @@ -135,22 +101,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn, html - - app = Robyn(__file__) - - - @app.get("/") - async def h(request): - html_string = "

Hello World

" - return html(html_string) - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, Request, html app = Robyn(__file__) @@ -179,21 +130,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn, serve_file - - app = Robyn(__file__) - - - @app.get("/") - async def h(request): - return serve_file("./index.html", file_name="index.html") # file_name is optional - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, serve_file, Request app = Robyn(__file__) @@ -222,23 +159,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn, serve_file - - app = Robyn(__file__) - - - app.serve_directory( - route="/test_dir", - directory_path=os.path.join(current_file_path, "build"), - index_file="index.html", - ) - - app.start(port=8080) - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, serve_file, Request app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/zh/api_reference/form_data.mdx b/docs_src/src/pages/documentation/zh/api_reference/form_data.mdx index b54d601d0..cf783d0d3 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/form_data.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/form_data.mdx @@ -14,14 +14,7 @@ export const description = '在此页面中,我们将深入了解如何处理 - ```python {{ title: 'untyped' }} - @app.post("/upload") - async def upload(request): - form_data = request.form_data - - return form_data - ``` - ```python {{ title: 'typed' }} + ```python @app.post("/upload") async def upload(request: Request): form_data = request.form_data diff --git a/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx b/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx index d18da118b..bb4ed185c 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/getting_started.mdx @@ -13,19 +13,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn - - app = Robyn(__file__) - - @app.get("/") - def h(request): - return "Hello, world" - - app.start(port=8080, host="0.0.0.0") # host 是可选的,默认为 127.0.0.1 - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Robyn, Request app = Robyn(__file__) @@ -34,10 +22,7 @@ export const description = def h(request: Request): return "Hello, world" - app.start(port=8080, host="0.0.0.0") - - - + app.start(port=8080, host="0.0.0.0") # host 是可选的,默认为 127.0.0.1 ``` @@ -51,14 +36,7 @@ export const description = - ```python {{ title: 'untyped' }} - @app.get("/") - async def h(request): - return "Hello, world" - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -166,14 +144,7 @@ export const description = - ```python {{ title: 'untyped' }} - @app.post("/") - async def h(request): - return "Hello World" - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.post("/") @@ -200,16 +171,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import jsonify - - @app.post("/jsonify") - async def json(request): - return {"hello": "world"} - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import jsonify, Request @@ -239,27 +201,14 @@ Robyn 向蝙蝠侠展示了如何从请求中访问路径参数和查询参数 - ```python {{ title: 'untyped' }} - from robyn import jsonify - - @app.post("/jsonify/:id") - async def json(request, path_params): - print(request.path_params["id"]) - print(path_params["id"]) - assert request.path_params["id"] == path_params["id"] - return {"hello": "world"} - - - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import jsonify from robyn.types import PathParams @app.post("/jsonify/:id") async def json(req_obj: Request, path_parameters: PathParams): print(req_obj.path_params["id"]) - print(path_params["id"]) + print(path_parameters["id"]) assert req_obj.path_params["id"] == path_parameters["id"] return {"hello": "world"} @@ -281,15 +230,7 @@ Robyn 向蝙蝠侠展示了如何从请求中访问路径参数和查询参数 - ```python {{ title: 'untyped' }} - @app.get("/query") - async def query_get(request, query_params): - query_data = query_params.to_dict() - assert query_data == request.query_params.to_dict() - return jsonify(query_data) - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request from robyn.robyn import QueryParams @@ -383,19 +324,7 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: - ```python {{ title: 'untyped' }} - @app.post("/dictionary") - async def dictionary(request): - return { - "status_code": 200, - "description": "This is a regular response", - "type": "text", - "headers": {"Header": "header_value"}, - } - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.post("/dictionary") @@ -407,7 +336,6 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: "headers": {"Header": "header_value"}, } - ``` @@ -424,15 +352,7 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: - ```python {{ title: 'untyped' }} - from robyn.robyn import Response - - @app.get("/response") - async def response(request): - return Response(status_code=200, headers=Headers({}), description="OK") - ``` - - ```python {{title: 'typed'}} + ```python from robyn.robyn import Response, Request @app.get("/response") @@ -455,31 +375,7 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: - ```python {{ title: 'untyped' }} - @app.get("/binary_output_response_sync") - def binary_output_response_sync(request): - return Response( - status_code=200, - headers={"Content-Type": "application/octet-stream"}, - description="OK", - ) - - - @app.get("/binary_output_async") - async def binary_output_async(request): - return b"OK" - - - @app.get("/binary_output_response_async") - async def binary_output_response_async(request): - return Response( - status_code=200, - headers={"Content-Type": "application/octet-stream"}, - description="OK", - ) - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request, Response @app.get("/binary_output_response_sync") @@ -525,17 +421,7 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: - ```python {{ title: 'untyped' }} - @app.get("/") - def binary_output_response_sync(request): - return Response( - status_code=200, - headers={"Content-Type": "application/octet-stream"}, - description="OK", - ) - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -561,11 +447,7 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: - ```python {{ title: 'untyped' }} - app.add_response_header("content-type", "application/json") - ``` - - ```python {{title: 'typed'}} + ```python app.add_response_header("content-type", "application/json") ``` @@ -579,11 +461,7 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: - ```python {{ title: 'untyped' }} - app.set_response_header("content-type", "application/json") - ``` - - ```python {{title: 'typed'}} + ```python app.set_response_header("content-type", "application/json") ``` @@ -596,11 +474,7 @@ Robyn 向蝙蝠侠演示了访问请求参数的不同语法示例: - ```python {{ title: 'untyped' }} - app.exclude_response_headers_for(["/login", "/signup"]) - ``` - - ```python {{title: 'typed'}} + ```python app.exclude_response_headers_for(["/login", "/signup"]) ``` @@ -618,17 +492,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - from robyn import Response, Headers - - @app.get("/") - def set_session(request): - response = Response(200, Headers({}), "Welcome!") - response.set_cookie(key="session", value="abc123") - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/") @@ -658,23 +522,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - @app.get("/login") - def login(request): - response = Response(200, Headers({}), "Logged in") - response.set_cookie( - key="auth_token", - value="secret123", - path="/", - max_age=3600, # 1 小时 - secure=True, # 仅 HTTPS - http_only=True, # 禁止 JavaScript 访问 - same_site="Strict", # CSRF 保护 - ) - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/login") @@ -705,15 +553,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - @app.get("/logout") - def logout(request): - response = Response(200, Headers({}), "Logged out") - response.cookies.delete("auth_token") - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/logout") @@ -736,28 +576,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - @app.get("/debug") - def debug_cookies(request): - response = Response(200, Headers({}), "Cookies set") - response.set_cookie("a", "1") - response.set_cookie("b", "2") - - # 获取所有 Cookie 名称 - names = response.cookies.keys() - - # 遍历 Cookie - for name in response.cookies: - print(f"Cookie: {name}") - - # 检查 Cookie 是否存在 - if "a" in response.cookies: - print("Cookie 'a' exists") - - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response, Headers @app.get("/debug") @@ -797,25 +616,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - @app.get("/") - def binary_output_response_sync(request): - headers = request.headers - - print("These are the request headers: ", headers) - existing_header = headers.get("exisiting_header") - existing_header = headers.get("exisiting_header", "default_value") - exisiting_header = headers["exisiting_header"] # This syntax is also valid - - headers.set("modified", "modified_value") - headers["new_header"] = "new_value" # This syntax is also valid - - print("These are the modified request headers: ", headers) - - return "" - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -833,7 +634,6 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 print("These are the modified request headers: ", headers) return "" - ``` @@ -848,11 +648,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - app.add_request_header("server", "robyn") - ``` - - ```python {{title: 'typed'}} + ```python app.add_request_header("server", "robyn") ``` @@ -868,11 +664,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - app.set_request_header("server", "robyn") - ``` - - ```python {{title: 'typed'}} + ```python app.set_request_header("server", "robyn") ``` @@ -892,15 +684,7 @@ Robyn 提供了符合 RFC 6265 标准的完整 Cookie API。使用 Response 对 - ```python {{ title: 'untyped' }} - from robyn import status_codes - - @app.get("/response") - async def response(request): - return Response(status_code=status_codes.HTTP_200_OK, headers=Headers({}), description="OK") - ``` - - ```python {{title: 'typed'}} + ```python from robyn import status_codes, Request diff --git a/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx b/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx index 7df7ea8d2..f73f087da 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/middlewares.mdx @@ -20,20 +20,15 @@ export const description = - ```python {{ title: 'untyped' }} + ```python async def startup_handler(): print("Starting up") app.startup_handler(startup_handler) - - ``` - - ```python {{title: 'typed'}} @app.shutdown_handler def shutdown_handler(): print("Shutting down") - ``` @@ -48,14 +43,7 @@ export const description = - ```python {{ title: 'untyped' }} - @app.get("/") - async def h(request): - return "Hello, world" - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request @app.get("/") @@ -87,19 +75,7 @@ export const description = - ```python {{ title: 'untyped' }} - @app.before_request("/") - async def hello_before_request(request: Request): - request.headers["before"] = "sync_before_request" - return request - - @app.after_request("/") - def hello_after_request(response: Response): - response.headers.set("after", "sync_after_request") - return response - ``` - - ```python {{ title: 'typed' }} + ```python from robyn import Request, Response @app.before_request("/") diff --git a/docs_src/src/pages/documentation/zh/api_reference/multiprocess_execution.mdx b/docs_src/src/pages/documentation/zh/api_reference/multiprocess_execution.mdx index 2d13e3aa6..e7ac58e74 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/multiprocess_execution.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/multiprocess_execution.mdx @@ -21,33 +21,7 @@ Robyn 向他保证,确实支持在多进程环境中共享变量。换句话 - ```python {{ title: 'untyped' }} - import threading - import time - from multiprocessing import Value - - from robyn import Robyn, Request - - app = Robyn(__file__) - - count = Value("i", 0) - - def counter(): - while True: - count.value += 1 - time.sleep(0.2) - print(count.value, "added 1") - - @app.get("/") - def index(request): - return f"{count.value}" - - threading.Thread(target=counter, daemon=True).start() - - app.start() - ``` - - ```python {{ title: 'typed' }} + ```python import threading import time from multiprocessing import Value diff --git a/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx b/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx index adcd5b57c..2e564e5e6 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx @@ -28,62 +28,7 @@ python app.py --disable-openapi -```python {{ title: 'untyped' }} -from robyn import Robyn -from robyn.robyn import QueryParams - -app = Robyn( - file_object=__file__, - openapi=OpenAPI( - info=OpenAPIInfo( - title="Sample App", - description="This is a sample server application.", - termsOfService="https://example.com/terms/", - version="1.0.0", - contact=Contact( - name="API Support", - url="https://www.example.com/support", - email="support@example.com", - ), - license=License( - name="BSD2.0", - url="https://opensource.org/license/bsd-2-clause", - ), - externalDocs=ExternalDocumentation(description="Find more info here", url="https://example.com/"), - components=Components(), - ), - ), -) - - -@app.get("/") -async def welcome(): - """欢迎""" - return "hi" - - -class GetRequestParams(QueryParams): - appointment_id: str - year: int - - -@app.get("/api/v1/name", openapi_name="Name Route", openapi_tags=["Name"]) -async def get(r, query_params: GetRequestParams): - """根据 ID 获取名称""" - return r.query_params - - -@app.delete("/users/:name", openapi_tags=["Name"]) -async def delete(r): - """根据名称删除用户""" - return r.path_params - - -if __name__ == "__main__": - app.start() -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Robyn, Request @@ -145,40 +90,7 @@ if __name__ == "__main__": -```python {{ title: 'untyped' }} -from robyn import SubRouter -from robyn.robyn import QueryParams - -subrouter = SubRouter(__name__, prefix="/sub") - - -@subrouter.get("/") -async def subrouter_welcome(): - """welcome subrouter""" - return "hiiiiii subrouter" - - -class SubRouterGetRequestParams(QueryParams): - _id: int - value: str - - -@subrouter.get("/name") -async def subrouter_get(r, query_params: SubRouterGetRequestParams): - """Get Name by ID""" - return r.query_params - - -@subrouter.delete("/:name") -async def subrouter_delete(r): - """Delete Name by ID""" - return r.path_params - - -app.include_router(subrouter) -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Request, SubRouter @@ -220,38 +132,7 @@ We support all the params mentioned in the latest OpenAPI specifications (https: -```python {{ title: 'untyped' }} -from robyn.types import JSONResponse, Body - -class Initial(Body): - is_present: bool - letter: Optional[str] - - -class FullName(Body): - first: str - second: str - initial: Initial - - -class CreateItemBody(Body): - name: FullName - description: str - price: float - tax: float - - -class CreateResponse(JSONResponse): - success: bool - items_changed: int - - -@app.post("/") -def create_item(request: Request, body: CreateItemBody) -> CreateResponse: - return CreateResponse(success=True, items_changed=2) -``` - -```python {{ title: 'typed' }} +```python from robyn.types import JSONResponse, Body class Initial(Body): diff --git a/docs_src/src/pages/documentation/zh/api_reference/redirection.mdx b/docs_src/src/pages/documentation/zh/api_reference/redirection.mdx index 84808f9d0..8bae2f197 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/redirection.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/redirection.mdx @@ -8,7 +8,7 @@ - ```python {{title: 'untyped'}} + ```python from robyn import Robyn, Response app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx b/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx index a4f464d56..5c5157ce4 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx @@ -42,24 +42,7 @@ identity (Optional[Identity]):客户端的身份 - ```python {{ title: 'untyped' }} - @dataclass - class Request: - """ - query_params: QueryParams - headers: Headers - path_params: dict[str, str] - body: Union[str, bytes] - method: str - url: Url - form_data: dict[str, str] - files: dict[str, bytes] - ip_addr: Optional[str] - identity: Optional[Identity] - """ - ``` - - ```python {{ title: 'typed' }} + ```python @dataclass class Request: """ diff --git a/docs_src/src/pages/documentation/zh/api_reference/scaling.mdx b/docs_src/src/pages/documentation/zh/api_reference/scaling.mdx index 16263174d..9b4416681 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/scaling.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/scaling.mdx @@ -8,11 +8,7 @@ - ```python {{ title: 'untyped' }} - python3 app.py --workers=N --process=M - ``` - - ```python {{title: 'typed'}} + ```python python3 app.py --workers=N --process=M ``` diff --git a/docs_src/src/pages/documentation/zh/api_reference/templating.mdx b/docs_src/src/pages/documentation/zh/api_reference/templating.mdx index fb449b176..b9fd3ba30 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/templating.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/templating.mdx @@ -13,22 +13,7 @@ export const description = - ```python {{title: 'untyped'}} - from robyn.templating import JinjaTemplate - - current_file_path = pathlib.Path(__file__).parent.resolve() - JINJA_TEMPLATE = JinjaTemplate(os.path.join(current_file_path, "templates")) - - @app.get("/template_render") - def template_render(): - context = {"framework": "Robyn", "templating_engine": "Jinja2"} - - template = JINJA_TEMPLATE.render_template(template_name="test.html", **context) - return template - - ``` - - ```python {{title: 'typed'}} + ```python from robyn.templating import JinjaTemplate current_file_path = pathlib.Path(__file__).parent.resolve() @@ -82,11 +67,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn.templating import TemplateInterface - ``` - - ```python {{ title: 'typed' }} + ```python from robyn.templating import TemplateInterface ``` @@ -101,20 +82,7 @@ export const description = - ```python {{ title: 'untyped' }} - class JinjaTemplate(TemplateInterface): - def __init__(self, directory, encoding="utf-8", followlinks=False): - self.env = Environment( - loader=FileSystemLoader( - searchpath=directory, encoding=encoding, followlinks=followlinks - ) - ) - - def render_template(self, template_name, **kwargs): - return self.env.get_template(template_name).render(**kwargs) - ``` - - ```python {{ title: 'typed' }} + ```python class JinjaTemplate(TemplateInterface): def __init__(self, directory, encoding="utf-8", followlinks=False): self.env = Environment( diff --git a/docs_src/src/pages/documentation/zh/api_reference/using_rust_directly.mdx b/docs_src/src/pages/documentation/zh/api_reference/using_rust_directly.mdx index a0147ddad..2e50a40a9 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/using_rust_directly.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/using_rust_directly.mdx @@ -10,11 +10,7 @@ - ```python {{ title: 'untyped' }} - python -m robyn --create-rust-file hello_world - ``` - - ```python {{title: 'typed'}} + ```python python -m robyn --create-rust-file hello_world ``` @@ -88,13 +84,7 @@ - ```python {{ title: 'untyped' }} - from hello_world import square - - print(square(5)) - ``` - - ```python {{title: 'typed'}} + ```python from hello_world import square print(square(5)) @@ -108,11 +98,7 @@ - ```python {{ title: 'untyped' }} - python -m robyn --compile-rust-path "." --dev - ``` - - ```python {{title: 'typed'}} + ```python python -m robyn --compile-rust-path "." --dev ``` diff --git a/docs_src/src/pages/documentation/zh/api_reference/views.mdx b/docs_src/src/pages/documentation/zh/api_reference/views.mdx index 5332d6345..2e215d30c 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/views.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/views.mdx @@ -16,18 +16,7 @@ export const description = - ```python {{ title: 'untyped' }} - def sample_view(): - def get(): - return "Hello, world!" - - def post(request): - body = request.body - return Response({"status_code": 200, "description": body, "headers": {}}) - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Request def sample_view(): @@ -54,18 +43,7 @@ export const description = - ```python {{ title: 'untyped' }} - @app.view("/sync/view/decorator") - def sync_decorator_view(): - def get(): - return "Hello, world!" - - def post(request): - body = request.body - return body - - ``` - ```python {{ title: 'typed' }} + ```python from robyn import Request @app.view("/sync/view/decorator") @@ -90,15 +68,7 @@ export const description = - ```python {{ title: 'untyped' }} - from .views import sample_view - - ... - ... - - app.add_view("/", sample_view) - ``` - ```python {{ title: 'typed' }} + ```python from .views import sample_view ... @@ -129,28 +99,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn, SubRouter - - app = Robyn(__file__) - - sub_router = SubRouter("/sub_router") - - @sub_router.get("/hello") - def hello(): - return "Hello, world" - - web_socket = SubRouter("/web_socket") - - @web_socket.message() - async def hello(): - return "Hello, world" - - app.include_router(sub_router) - ``` - - - ```python {{ title: 'typed' }} + ```python from robyn import Robyn, SubRouter app = Robyn(__file__) diff --git a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx index c96fe128f..cfb6fe0c3 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx @@ -15,28 +15,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn, jsonify, WebSocket - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("message") - def connect(): - return "Hello world, from ws" - - @websocket.on("close") - def close(): - return "Goodbye world, from ws" - - @websocket.on("connect") - def message(): - return "Connected to ws" - - - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Robyn, jsonify, WebSocket app = Robyn(__file__) @@ -76,31 +55,7 @@ export const description = - ```python {{ title: 'untyped' }} - from robyn import Robyn, WebSocket - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("connect") - async def connect(): - # 无需返回 - 仅记录连接 - print("客户端已连接") - - @websocket.on("message") - def message(ws, msg): - # 处理消息但不响应 - process_analytics(msg) - # 无需返回语句 - - @websocket.on("close") - async def close(): - # 显式返回 None - 不发送消息 - cleanup_resources() - return None - ``` - - ```python {{title: 'typed'}} + ```python from robyn import Robyn, WebSocket, WebSocketConnector app = Robyn(__file__) @@ -135,17 +90,7 @@ export const description = - ```python {{ title: 'untyped' }} - - @websocket.on("message") - def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - ws.sync_send_to(websocket_id, "This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -168,17 +113,7 @@ export const description = - ```python {{ title: 'untyped' }} - - @websocket.on("message") - async def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - await ws.async_send_to(websocket_id, "This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -200,17 +135,7 @@ export const description = - ```python {{ title: 'untyped' }} - - @websocket.on("message") - def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - ws.sync_broadcast("This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -231,17 +156,7 @@ export const description = - ```python {{ title: 'untyped' }} - - @websocket.on("message") - async def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - await ws.async_broadcast("This is a message to self") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -262,18 +177,7 @@ export const description = - ```python {{ title: 'untyped' }} - - @websocket.on("message") - async def message(ws, msg, global_dependencies) -> str: - websocket_id = ws.id - if (ws.query_params.get("name") == "gordon" and ws.query_params.get("desg") == "commissioner"): - ws.sync_broadcast("Gordon authorized to login!") - return "" - - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: @@ -301,16 +205,7 @@ export const description = - ```python {{ title: 'untyped' }} - @websocket.on("message") - def message(ws, msg): - if msg == "disconnect": - ws.close() - return "Closing connection" - return "Message received" - ``` - - ```python {{title: 'typed'}} + ```python @websocket.on("message") def message(ws: WebSocketConnector, msg: str) -> str: if msg == "disconnect": diff --git a/docs_src/src/pages/documentation/zh/example_app/openapi.mdx b/docs_src/src/pages/documentation/zh/example_app/openapi.mdx index bd304c0fa..82b81f565 100644 --- a/docs_src/src/pages/documentation/zh/example_app/openapi.mdx +++ b/docs_src/src/pages/documentation/zh/example_app/openapi.mdx @@ -28,62 +28,7 @@ python app.py --disable-openapi -```python {{ title: 'untyped' }} -from robyn import Robyn -from robyn.robyn import QueryParams - -app = Robyn( - file_object=__file__, - openapi=OpenAPI( - info=OpenAPIInfo( - title="Sample App", - description="This is a sample server application.", - termsOfService="https://example.com/terms/", - version="1.0.0", - contact=Contact( - name="API Support", - url="https://www.example.com/support", - email="support@example.com", - ), - license=License( - name="BSD2.0", - url="https://opensource.org/license/bsd-2-clause", - ), - externalDocs=ExternalDocumentation(description="Find more info here", url="https://example.com/"), - components=Components(), - ), - ), -) - - -@app.get("/") -async def welcome(): - """欢迎""" - return "hi" - - -class GetRequestParams(QueryParams): - appointment_id: str - year: int - - -@app.get("/api/v1/name", openapi_name="Name Route", openapi_tags=["Name"]) -async def get(r, query_params: GetRequestParams): - """根据 ID 获取名称""" - return r.query_params - - -@app.delete("/users/:name", openapi_tags=["Name"]) -async def delete(r): - """根据名称删除用户""" - return r.path_params - - -if __name__ == "__main__": - app.start() -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Robyn, Request @@ -145,40 +90,7 @@ if __name__ == "__main__": -```python {{ title: 'untyped' }} -from robyn import SubRouter -from robyn.robyn import QueryParams - -subrouter = SubRouter(__name__, prefix="/sub") - - -@subrouter.get("/") -async def subrouter_welcome(): - """欢迎来到子路由""" - return "hiiiiii subrouter" - - -class SubRouterGetRequestParams(QueryParams): - _id: int - value: str - - -@subrouter.get("/name") -async def subrouter_get(r, query_params: SubRouterGetRequestParams): - """"根据 ID 获取名称"" - return r.query_params - - -@subrouter.delete("/:name") -async def subrouter_delete(r): - """根据名称删除用户""" - return r.path_params - - -app.include_router(subrouter) -``` - -```python {{ title: 'typed' }} +```python from robyn.robyn import QueryParams from robyn import Request, SubRouter @@ -220,38 +132,7 @@ Robyn 支持最新的 OpenAPI 规范([https://swagger.io/specification/](https -```python {{ title: 'untyped' }} -from robyn.types import JSONResponse, Body - -class Initial(Body): - is_present: bool - letter: Optional[str] - - -class FullName(Body): - first: str - second: str - initial: Initial - - -class CreateItemBody(Body): - name: FullName - description: str - price: float - tax: float - - -class CreateResponse(JSONResponse): - success: bool - items_changed: int - - -@app.post("/") -def create_item(request: Request, body: CreateItemBody) -> CreateResponse: - return CreateResponse(success=True, items_changed=2) -``` - -```python {{ title: 'typed' }} +```python from robyn.types import JSONResponse, Body class Initial(Body): From 3a53ef69ec28b6c14cc68d7475b839b153be7b2c Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 8 Feb 2026 03:46:23 +0000 Subject: [PATCH 032/106] feat: add missing default headers (#1306) * feat: add missing default headers * update --- integration_tests/test_status_code.py | 33 ++++++++++++++++++++++++++- src/types/response.rs | 12 +++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/integration_tests/test_status_code.py b/integration_tests/test_status_code.py index a81299052..735b6be3a 100644 --- a/integration_tests/test_status_code.py +++ b/integration_tests/test_status_code.py @@ -1,6 +1,7 @@ import pytest +import requests -from integration_tests.helpers.http_methods_helpers import get +from integration_tests.helpers.http_methods_helpers import BASE_URL, get @pytest.mark.benchmark @@ -23,3 +24,33 @@ def test_202_status_code(session): @pytest.mark.parametrize("function_type", ["sync", "async"]) def test_sync_500_internal_server_error(function_type: str, session): get(f"/{function_type}/raise", expected_status_code=500) + + +# ===== Content-Type on error responses ===== + + +@pytest.mark.benchmark +def test_404_not_found_content_type(session): + """A request to a non-existent route should return Content-Type: text/plain""" + r = get("/real/404", expected_status_code=404) + assert r.text == "Not found" + content_type = r.headers.get("Content-Type", "") + assert "text/plain" in content_type + + +@pytest.mark.benchmark +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_500_error_content_type(function_type: str, session): + """An unhandled exception should return Content-Type: text/plain""" + r = get(f"/{function_type}/raise", expected_status_code=500) + content_type = r.headers.get("Content-Type", "") + assert "text/plain" in content_type + + +@pytest.mark.benchmark +def test_405_method_not_allowed_content_type(session): + """An unsupported HTTP method should return 405 with Content-Type: text/plain""" + response = requests.request("NONSTANDARD", f"{BASE_URL}/") + assert response.status_code == 405 + content_type = response.headers.get("Content-Type", "") + assert "text/plain" in content_type diff --git a/src/types/response.rs b/src/types/response.rs index 09bc7ffcb..ec8b3dc6a 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -185,13 +185,19 @@ fn create_python_stream( } impl Response { + fn default_text_headers() -> Headers { + let mut headers = Headers::new(None); + headers.set("Content-Type".to_string(), "text/plain".to_string()); + headers + } + pub fn not_found(headers: Option<&Headers>) -> Self { const NOT_FOUND_BYTES: &[u8] = b"Not found"; Self { status_code: 404, response_type: "text".to_string(), - headers: headers.cloned().unwrap_or_else(|| Headers::new(None)), + headers: headers.cloned().unwrap_or_else(Self::default_text_headers), description: NOT_FOUND_BYTES.to_vec(), file_path: None, cookies: Cookies::new(), @@ -204,7 +210,7 @@ impl Response { Self { status_code: 500, response_type: "text".to_string(), - headers: headers.cloned().unwrap_or_else(|| Headers::new(None)), + headers: headers.cloned().unwrap_or_else(Self::default_text_headers), description: SERVER_ERROR_BYTES.to_vec(), file_path: None, cookies: Cookies::new(), @@ -217,7 +223,7 @@ impl Response { Self { status_code: 405, response_type: "text".to_string(), - headers: headers.cloned().unwrap_or_else(|| Headers::new(None)), + headers: headers.cloned().unwrap_or_else(Self::default_text_headers), description: METHOD_NOT_ALLOWED_BYTES.to_vec(), file_path: None, cookies: Cookies::new(), From 5b1e77fece7a70d198c51449516399530abcaff9 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 14 Feb 2026 02:40:46 +0000 Subject: [PATCH 033/106] fix: websockets (#1303) * fix: websockets * update * update * update * update * update * update * update * update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../en/api_reference/dependency_injection.mdx | 38 +++ .../en/api_reference/websockets.mdx | 304 ++++++++++------- .../zh/api_reference/dependency_injection.mdx | 38 +++ .../zh/api_reference/websockets.mdx | 309 +++++++++++------- integration_tests/base_routes.py | 185 ++++++----- integration_tests/test_web_sockets.py | 19 +- robyn/__init__.py | 46 ++- robyn/processpool.py | 23 +- robyn/robyn.pyi | 1 + robyn/router.py | 11 +- robyn/ws.py | 290 +++++++++++++++- src/lib.rs | 3 +- src/routers/web_socket_router.rs | 14 + src/server.rs | 13 +- src/websockets/mod.rs | 98 +++++- 15 files changed, 1022 insertions(+), 370 deletions(-) diff --git a/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx b/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx index 61859fb8b..f287086da 100644 --- a/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx @@ -71,6 +71,44 @@ Note: `router_dependencies`, `global_dependencies` are reserved parameters and *
+### WebSocket Dependency Injection + + + +WebSockets support the same dependency injection system as HTTP routes. The `global_dependencies` and `router_dependencies` parameters work in the main handler, `on_connect`, and `on_close` callbacks. + + + + ```python {{ title: 'WebSocket DI' }} + from robyn import Robyn + import logging + + app = Robyn(__file__) + + app.inject_global(logger=logging.getLogger(__name__)) + app.inject(cache=RedisCache()) + + @app.websocket("/chat") + async def chat(websocket, global_dependencies=None, router_dependencies=None): + logger = global_dependencies.get("logger") + cache = router_dependencies.get("cache") + logger.info(f"New connection: {websocket.id}") + + while True: + message = await websocket.receive_text() + cache.set(f"ws_{websocket.id}", message) + await websocket.broadcast(f"User {websocket.id}: {message}") + + @chat.on_connect + async def on_connect(websocket, global_dependencies=None): + logger = global_dependencies.get("logger") + logger.info(f"Client connected: {websocket.id}") + return "Connected" + ``` + + + + --- diff --git a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx index 6e508860f..6258c1e63 100644 --- a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx @@ -1,5 +1,5 @@ export const description = - 'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.' + 'Learn how to use Robyn\'s WebSocket API for real-time, bidirectional communication — including message streaming, connect/close callbacks, broadcasting, and common patterns like live updates, presence tracking, and low-latency messaging.' ## WebSockets {{ tag: 'WebSockets', label: 'WebSockets' }} @@ -10,30 +10,49 @@ After mastering [Server-Sent Events](/documentation/en/api_reference/server_sent "SSE is great for pushing updates to my dashboard," Batman thought, "but I need two-way communication for coordinating with my allies!" -To handle real-time bidirectional communication, Batman learned how to work with WebSockets. He created a WebSocket class and wrapped it around his Robyn app: +To handle real-time bidirectional communication, Batman learned how to work with WebSockets using Robyn's modern decorator-based API. Under the hood, messages flow through Rust channels for maximum performance — no Python GIL overhead during message dispatch. - + - ```python - from robyn import Robyn, jsonify, WebSocket + ```python {{ title: 'Basic Echo' }} + from robyn import Robyn app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - @websocket.on("message") - def connect(): - return "Hello world, from ws" + @app.websocket("/web_socket") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") - @websocket.on("close") - def close(): - return "Goodbye world, from ws" + app.start() + ``` + + ```python {{ title: 'With Callbacks' }} + from robyn import Robyn, WebSocketDisconnect + + app = Robyn(__file__) - @websocket.on("connect") - def message(): + @app.websocket("/web_socket") + async def handler(websocket): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") + except WebSocketDisconnect: + print(f"Client {websocket.id} disconnected") + + @handler.on_connect + def on_connect(websocket): return "Connected to ws" + @handler.on_close + def on_close(websocket): + return "Goodbye world, from ws" + + app.start() ``` @@ -41,174 +60,237 @@ To handle real-time bidirectional communication, Batman learned how to work with --- -## Optional Return Values {{ tag: 'Optional Returns', label: 'Optional Returns' }} +## Receiving Messages {{ tag: 'receive_text', label: 'receive_text' }} - Batman discovered that WebSocket handlers don't always need to return a value. Sometimes, he just wanted to process a message or perform an action without sending a response back to the client. + The `receive_text()` method blocks until the next message arrives from the client. It is backed by a Rust `tokio::mpsc` channel, so the Python handler genuinely suspends without holding the GIL. - "Not every message needs a reply," Batman realized. "Sometimes I just need to log data or trigger an action." - - WebSocket handlers (`connect`, `message`, and `close`) can optionally return a string. If no value is returned (or `None` is returned), no message will be sent to the client. + When the client disconnects, `receive_text()` raises `WebSocketDisconnect`. You can either catch it explicitly or let the internal wrapper handle it silently. - - - ```python - from robyn import Robyn, WebSocket, WebSocketConnector - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("connect") - async def connect() -> None: - # No return needed - just log the connection - print("Client connected") - - @websocket.on("message") - def message(ws: WebSocketConnector, msg: str) -> None: - # Process message without responding - process_analytics(msg) - # No return statement needed + + + ```python {{ title: 'Text Messages' }} + @app.websocket("/ws") + async def handler(websocket): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Got: {msg}") + except WebSocketDisconnect: + print(f"Client {websocket.id} disconnected") + ``` - @websocket.on("close") - async def close() -> None: - # Explicitly return None - no message sent - cleanup_resources() - return None + ```python {{ title: 'JSON Messages' }} + @app.websocket("/api") + async def handler(websocket): + while True: + data = await websocket.receive_json() + result = process(data) + await websocket.send_json({"status": "ok", "result": result}) ``` - +--- + +## Sending Messages {{ tag: 'send_text', label: 'send_text' }} + - For sending a message to the client, Batman used the `sync_send_to` method. + To send a message to the current client, use `send_text()` or `send_json()`. All send methods are async. - - - ```python - - @websocket.on("message") - def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - ws.sync_send_to(websocket_id, "This is a message to self") - return "" + + + ```python {{ title: 'Send Text' }} + @app.websocket("/ws") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") + ``` + ```python {{ title: 'Send JSON' }} + @app.websocket("/ws") + async def handler(websocket): + while True: + data = await websocket.receive_json() + await websocket.send_json({"echo": data}) ``` +--- - +## Broadcasting {{ tag: 'broadcast', label: 'broadcast' }} + - For sending a message to the client in async manner, Batman used the `async_send_to` method. + To send a message to all connected clients on the same WebSocket endpoint, use the `broadcast()` method. - - - ```python - - @websocket.on("message") - async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - await ws.async_send_to(websocket_id, "This is a message to self") - return "" - + + + ```python {{ title: 'Broadcast' }} + @app.websocket("/chat") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + # Send to all connected clients + await websocket.broadcast(f"User {websocket.id}: {msg}") + # Also send a confirmation to this client only + await websocket.send_text("Your message was sent") ``` +--- + +## Query Parameters {{ tag: 'query_params', label: 'query_params' }} - For sending broadcast messages, Batman used the `sync_broadcast` method. + You can access query parameters from the WebSocket connection URL via `websocket.query_params`. - + - ```python + ```python {{ title: 'Query Params' }} + @app.websocket("/ws") + async def handler(websocket): + name = websocket.query_params.get("name") + role = websocket.query_params.get("role") - @websocket.on("message") - def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - ws.sync_broadcast("This is a message to self") - return "" + if name == "gordon" and role == "commissioner": + await websocket.broadcast("Gordon authorized!") + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Hello {name}: {msg}") ``` +--- + +## Closing Connections {{ tag: 'close', label: 'close' }} + - For sending broadcast messages in async style, Batman used the `async_broadcast` method. + To programmatically close a WebSocket connection from the server side, use `websocket.close()`. This will: +1. Close the WebSocket connection. +2. Remove the client from the WebSocket registry. +3. Cause any pending `receive_text()` to raise `WebSocketDisconnect`. - - - ```python - - @websocket.on("message") - async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - await ws.async_broadcast("This is a message to self") - return "" - + + + ```python {{ title: 'Server Close' }} + @app.websocket("/ws") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + if msg == "quit": + await websocket.close() + break + await websocket.send_text(f"Got: {msg}") ``` +--- + +## Connect and Close Callbacks {{ tag: 'Callbacks', label: 'Callbacks' }} + - Robyn also showed Batman to work with query params. - - - + You can attach optional `on_connect` and `on_close` callbacks to your WebSocket handler. These are decorators on the handler function itself. - ```python - - @websocket.on("message") - async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - if (ws.query_params.get("name") == "gordon" and ws.query_params.get("desg") == "commissioner"): - ws.sync_broadcast("Gordon authorized to login!") - return "" + - `on_connect` is called when a new client connects. Its return value is sent to the client as the first message. + - `on_close` is called when the connection closes. Its return value is sent to the client as the final message. + Both callbacks receive a `websocket` object with access to `id` and `query_params`. Both are optional. + + + + + ```python {{ title: 'Callbacks' }} + @app.websocket("/chat") + async def chat(websocket): + while True: + msg = await websocket.receive_text() + await websocket.broadcast(msg) + + @chat.on_connect + def on_connect(websocket): + return f"Welcome, {websocket.id}!" + + @chat.on_close + def on_close(websocket): + return "Goodbye!" ``` +--- +## WebSocket API Reference {{ tag: 'API', label: 'API' }} - To programmatically close a WebSocket connection from the server side, Batman learned to use the `close()` method: - The `close()` method does the following: -1. Sends a close message to the client. -2. Removes the client from the WebSocket registry. -3. Closes the WebSocket connection. + The `websocket` object passed to handlers exposes the following methods and properties: + + | Method / Property | Description | + |---|---| + | `await websocket.receive_text()` | Block until next message; raises `WebSocketDisconnect` on close | + | `await websocket.receive_bytes()` | Block until next binary message; raises `WebSocketDisconnect` on close | + | `await websocket.receive_json()` | Same as `receive_text()` but JSON-decoded | + | `await websocket.send_text(data)` | Send string to this client | + | `await websocket.send_bytes(data)` | Send binary data to this client | + | `await websocket.send_json(data)` | Send JSON to this client | + | `await websocket.broadcast(data)` | Send to all clients on this endpoint | + | `await websocket.close()` | Close the connection server-side | + | `websocket.id` | Connection UUID string | + | `websocket.query_params` | Query parameters from the connection URL | + + -This method is useful for scenarios where you need to programmatically end a WebSocket connection based on certain conditions or events on the server side. +--- - +## Legacy API {{ tag: 'Legacy', label: 'Legacy' }} + + - + The old event-based WebSocket API is still supported for backward compatibility. If you have existing code using `WebSocket(app, "/ws")` with `@websocket.on("message")`, it will continue to work without changes. + + + + + ```python {{ title: 'Legacy Style' }} + from robyn import Robyn, WebSocket + + app = Robyn(__file__) + websocket = WebSocket(app, "/web_socket") + + @websocket.on("connect") + def connect(): + return "Hello world, from ws" - ```python @websocket.on("message") - def message(ws: WebSocketConnector, msg: str) -> str: - if msg == "disconnect": - ws.close() - return "Closing connection" - return "Message received" + def message(ws, msg): + return f"Echo: {msg}" + + @websocket.on("close") + def close(): + return "Goodbye world, from ws" ``` @@ -226,5 +308,3 @@ Robyn told him about the different ways he could scale his application, and how - [Views and SubRouters](/documentation/en/api_reference/views) - - diff --git a/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx b/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx index 55c16cc18..bb33cc1fa 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/dependency_injection.mdx @@ -69,6 +69,44 @@ Robyn 提供了两种依赖注入方式: +### WebSocket 依赖注入 + + + +WebSocket 支持与 HTTP 路由相同的依赖注入系统。`global_dependencies` 和 `router_dependencies` 参数可以在主处理程序、`on_connect` 和 `on_close` 回调中使用。 + + + + ```python {{ title: 'WebSocket 依赖注入' }} + from robyn import Robyn + import logging + + app = Robyn(__file__) + + app.inject_global(logger=logging.getLogger(__name__)) + app.inject(cache=RedisCache()) + + @app.websocket("/chat") + async def chat(websocket, global_dependencies=None, router_dependencies=None): + logger = global_dependencies.get("logger") + cache = router_dependencies.get("cache") + logger.info(f"新连接: {websocket.id}") + + while True: + message = await websocket.receive_text() + cache.set(f"ws_{websocket.id}", message) + await websocket.broadcast(f"用户 {websocket.id}: {message}") + + @chat.on_connect + async def on_connect(websocket, global_dependencies=None): + logger = global_dependencies.get("logger") + logger.info(f"客户端已连接: {websocket.id}") + return "已连接" + ``` + + + + --- ## 下一步 diff --git a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx index cfb6fe0c3..02667635c 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx @@ -9,31 +9,46 @@ export const description = "SSE 很适合向我的仪表板推送更新,"蝙蝠侠想道,"但我需要双向通信来与我的盟友协调!" -为了实现双向实时通信,蝙蝠侠学习了如何使用 WebSocket。他创建了一个 WebSocket 类,并将其集成到他的 Robyn 应用中: +为了实现双向实时通信,蝙蝠侠学习了如何使用 Robyn 的现代装饰器 API 处理 WebSocket。底层消息通过 Rust 通道传递,实现最大性能——消息分发过程中无需 Python GIL。 - + - ```python - from robyn import Robyn, jsonify, WebSocket + ```python {{ title: '基础回声' }} + from robyn import Robyn app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - @websocket.on("message") - def connect(): - return "Hello world, from ws" + @app.websocket("/web_socket") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") - @websocket.on("close") - def close(): - return "Goodbye world, from ws" + app.start() + ``` + + ```python {{ title: '带回调' }} + from robyn import Robyn + + app = Robyn(__file__) + + @app.websocket("/web_socket") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") - @websocket.on("connect") - def message(): - return "Connected to ws" + @handler.on_connect + def on_connect(websocket): + return "已连接到 ws" + @handler.on_close + def on_close(websocket): + return "再见,来自 ws" + app.start() ``` @@ -42,179 +57,239 @@ export const description = --- -## 可选返回值 {{ tag: '可选返回值', label: '可选返回值' }} +## 接收消息 {{ tag: 'receive_text', label: 'receive_text' }} - 蝙蝠侠发现 WebSocket 处理程序并不总是需要返回值。有时,他只是想处理消息或执行某个操作,而不需要向客户端发送响应。 + `receive_text()` 方法会阻塞直到下一条消息到达。它由 Rust 的 `tokio::mpsc` 通道支持,因此 Python 处理程序在等待时不会持有 GIL。 - "并非每条消息都需要回复,"蝙蝠侠意识到,"有时我只需要记录数据或触发某个操作。" - - WebSocket 处理程序(`connect`、`message` 和 `close`)可以选择性地返回字符串。如果不返回值(或返回 `None`),则不会向客户端发送任何消息。 + 当客户端断开连接时,`receive_text()` 会抛出 `WebSocketDisconnect` 异常。您可以显式捕获它,也可以让内部包装器静默处理。 - - - ```python - from robyn import Robyn, WebSocket, WebSocketConnector - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("connect") - async def connect() -> None: - # 无需返回 - 仅记录连接 - print("客户端已连接") - - @websocket.on("message") - def message(ws: WebSocketConnector, msg: str) -> None: - # 处理消息但不响应 - process_analytics(msg) - # 无需返回语句 + + + ```python {{ title: '文本消息' }} + @app.websocket("/ws") + async def handler(websocket): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"收到: {msg}") + except WebSocketDisconnect: + print(f"客户端 {websocket.id} 已断开") + ``` - @websocket.on("close") - async def close() -> None: - # 显式返回 None - 不发送消息 - cleanup_resources() - return None + ```python {{ title: 'JSON 消息' }} + @app.websocket("/api") + async def handler(websocket): + while True: + data = await websocket.receive_json() + result = process(data) + await websocket.send_json({"status": "ok", "result": result}) ``` - +--- +## 发送消息 {{ tag: 'send_text', label: 'send_text' }} + + - 为了向客户端发送消息,蝙蝠侠使用了 `sync_send_to` 方法。 + 使用 `send_text()` 或 `send_json()` 向当前客户端发送消息。所有发送方法都是异步的。 - - - ```python - - @websocket.on("message") - def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - state = websocket_state[websocket_id] - ws.sync_send_to(websocket_id, "This is a message to self") - return "" + + + ```python {{ title: '发送文本' }} + @app.websocket("/ws") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") + ``` + ```python {{ title: '发送 JSON' }} + @app.websocket("/ws") + async def handler(websocket): + while True: + data = await websocket.receive_json() + await websocket.send_json({"echo": data}) ``` - - +--- + +## 广播 {{ tag: 'broadcast', label: 'broadcast' }} + - 为了异步向客户端发送消息,蝙蝠侠使用了 `async_send_to` 方法。 + 使用 `broadcast()` 方法向同一 WebSocket 端点上的所有已连接客户端发送消息。 - - - ```python - - @websocket.on("message") - async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - state = websocket_state[websocket_id] - await ws.async_send_to(websocket_id, "This is a message to self") - return "" - + + + ```python {{ title: '广播' }} + @app.websocket("/chat") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + # 向所有已连接的客户端发送 + await websocket.broadcast(f"用户 {websocket.id}: {msg}") + # 仅向当前客户端发送确认 + await websocket.send_text("您的消息已发送") ``` - +--- + +## 查询参数 {{ tag: 'query_params', label: 'query_params' }} + - 为了向所有客户端发送广播,蝙蝠侠使用了 `sync_broadcast` 方法。 + 通过 `websocket.query_params` 访问 WebSocket 连接 URL 中的查询参数。 - + - ```python + ```python {{ title: '查询参数' }} + @app.websocket("/ws") + async def handler(websocket): + name = websocket.query_params.get("name") + role = websocket.query_params.get("role") - @websocket.on("message") - def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - ws.sync_broadcast("This is a message to self") - return "" + if name == "gordon" and role == "commissioner": + await websocket.broadcast("戈登已授权!") + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"你好 {name}: {msg}") ``` - +--- + +## 关闭连接 {{ tag: 'close', label: 'close' }} + - 为了异步发送广播,蝙蝠侠使用了 `async_broadcast` 方法。 + 使用 `websocket.close()` 从服务端关闭 WebSocket 连接。该方法将: +1. 关闭 WebSocket 连接。 +2. 从 WebSocket 注册表中移除客户端。 +3. 使任何挂起的 `receive_text()` 抛出 `WebSocketDisconnect` 异常。 - - - ```python - - @websocket.on("message") - async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - await ws.async_broadcast("This is a message to self") - return "" - + + + ```python {{ title: '服务端关闭' }} + @app.websocket("/ws") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + if msg == "quit": + await websocket.close() + break + await websocket.send_text(f"收到: {msg}") ``` - +--- + +## 连接和关闭回调 {{ tag: '回调', label: '回调' }} + - 此外,Robyn 还向蝙蝠侠展示了如何处理 WebSocket 查询参数。 - - - + 您可以为 WebSocket 处理程序附加可选的 `on_connect` 和 `on_close` 回调。它们是处理函数本身的装饰器。 - ```python - - @websocket.on("message") - async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - websocket_id = ws.id - if (ws.query_params.get("name") == "gordon" and ws.query_params.get("desg") == "commissioner"): - ws.sync_broadcast("Gordon authorized to login!") - return "" + - `on_connect` 在新客户端连接时调用。其返回值作为第一条消息发送给客户端。 + - `on_close` 在连接关闭时调用。其返回值作为最后一条消息发送给客户端。 + 两个回调都接收一个 `websocket` 对象,可以访问 `id` 和 `query_params`。两者都是可选的。 + + + + + ```python {{ title: '回调' }} + @app.websocket("/chat") + async def chat(websocket): + while True: + msg = await websocket.receive_text() + await websocket.broadcast(msg) + + @chat.on_connect + def on_connect(websocket): + return f"欢迎,{websocket.id}!" + + @chat.on_close + def on_close(websocket): + return "再见!" ``` - +--- + +## WebSocket API 参考 {{ tag: 'API', label: 'API' }} + - 蝙蝠侠还学习了如何通过 `close()` 方法从服务端关闭 WebSocket 连接。`close()` 方法将执行以下操作: -1. 向客户端发送关闭消息 -2. 从 WebSocket 注册表中移除客户端 -3. 关闭 WebSocket 连接 + 传递给处理程序的 `websocket` 对象提供以下方法和属性: + + | 方法 / 属性 | 描述 | + |---|---| + | `await websocket.receive_text()` | 阻塞直到下一条消息;连接关闭时抛出 `WebSocketDisconnect` | + | `await websocket.receive_bytes()` | 阻塞直到下一条二进制消息;连接关闭时抛出 `WebSocketDisconnect` | + | `await websocket.receive_json()` | 与 `receive_text()` 相同,但返回 JSON 解码后的数据 | + | `await websocket.send_text(data)` | 向当前客户端发送文本 | + | `await websocket.send_bytes(data)` | 向当前客户端发送二进制数据 | + | `await websocket.send_json(data)` | 向当前客户端发送 JSON | + | `await websocket.broadcast(data)` | 向此端点的所有客户端广播 | + | `await websocket.close()` | 从服务端关闭连接 | + | `websocket.id` | 连接 UUID 字符串 | + | `websocket.query_params` | 连接 URL 中的查询参数 | + + -这种方法适用于需要根据服务端某些条件或事件来结束 WebSocket 连接的场景。 +--- - +## 旧版 API {{ tag: '旧版', label: '旧版' }} + + - + 旧版基于事件的 WebSocket API 仍然支持向后兼容。如果您有使用 `WebSocket(app, "/ws")` 和 `@websocket.on("message")` 的现有代码,它将继续正常工作。 + + + + + ```python {{ title: '旧版风格' }} + from robyn import Robyn, WebSocket + + app = Robyn(__file__) + websocket = WebSocket(app, "/web_socket") + + @websocket.on("connect") + def connect(): + return "Hello world, from ws" - ```python @websocket.on("message") - def message(ws: WebSocketConnector, msg: str) -> str: - if msg == "disconnect": - ws.close() - return "Closing connection" - return "Message received" + def message(ws, msg): + return f"Echo: {msg}" + + @websocket.on("close") + def close(): + return "Goodbye world, from ws" ``` - diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 67891226c..31a6ece72 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -7,25 +7,13 @@ from typing import Optional from integration_tests.subroutes import di_subrouter, static_router, sub_router -from robyn import Headers, Request, Response, Robyn, SSEMessage, SSEResponse, WebSocket, WebSocketConnector, jsonify, serve_file, serve_html +from robyn import Headers, Request, Response, Robyn, SSEMessage, SSEResponse, WebSocketDisconnect, jsonify, serve_file, serve_html from robyn.authentication import AuthenticationHandler, BearerGetter, Identity from robyn.robyn import QueryParams, Url from robyn.templating import JinjaTemplate from robyn.types import Body, JSONResponse, Method, PathParams app = Robyn(__file__) -websocket = WebSocket(app, "/web_socket") - -# Creating a new WebSocket app to test json handling + to serve an example to future users of this lib -# while the original "raw" web_socket is used with benchmark tests -websocket_json = WebSocket(app, "/web_socket_json") - -websocket_di = WebSocket(app, "/web_socket_di") - -websocket_di.inject_global(GLOBAL_DEPENDENCY="GLOBAL DEPENDENCY") -websocket_di.inject(ROUTER_DEPENDENCY="ROUTER DEPENDENCY") - -websocket_empty_returns = WebSocket(app, "/web_socket_empty_returns") current_file_path = pathlib.Path(__file__).parent.resolve() jinja_template = JinjaTemplate(os.path.join(current_file_path, "templates")) @@ -36,100 +24,125 @@ websocket_state = defaultdict(int) -@websocket_json.on("message") -async def jsonws_message(ws, msg: str) -> str: - websocket_id = ws.id - response: dict = {"ws_id": websocket_id, "resp": "", "msg": msg} - global websocket_state - state = websocket_state[websocket_id] - if state == 0: - response["resp"] = "Whaaat??" - elif state == 1: - response["resp"] = "Whooo??" - elif state == 2: - response["resp"] = "*chika* *chika* Slim Shady." - websocket_state[websocket_id] = (state + 1) % 3 - return jsonify(response) - - -@websocket.on("message") -async def message(ws: WebSocketConnector, msg: str, global_dependencies) -> str: - global websocket_state - websocket_id = ws.id - state = websocket_state[websocket_id] - resp = "" - if state == 0: - resp = "Whaaat??" - await ws.async_broadcast("This is a broadcast message") - ws.sync_send_to(websocket_id, "This is a message to self") - elif state == 1: - resp = "Whooo??" - elif state == 2: - await ws.async_broadcast(ws.query_params.get("one", None)) - ws.sync_send_to(websocket_id, ws.query_params.get("two", None)) - resp = "*chika* *chika* Slim Shady." - elif state == 3: - ws.close() - # TODO temporary fix to avoid CI failure - resp = "Connection closed" - - websocket_state[websocket_id] = (state + 1) % 4 - return resp - - -@websocket.on("close") -def close(): - return "GoodBye world, from ws" +# --- Regular WebSocket endpoint (new-style with Rust channels) --- +@app.websocket("/web_socket") +async def websocket_endpoint(websocket): + try: + while True: + _ = await websocket.receive_text() + websocket_id = websocket.id + global websocket_state + state = websocket_state[websocket_id] + + if state == 0: + await websocket.broadcast("This is a broadcast message") + await websocket.send_text("This is a message to self") + await websocket.send_text("Whaaat??") + elif state == 1: + await websocket.send_text("Whooo??") + elif state == 2: + await websocket.broadcast(websocket.query_params.get("one", "")) + await websocket.send_text(websocket.query_params.get("two", "")) + await websocket.send_text("*chika* *chika* Slim Shady.") + elif state == 3: + await websocket.send_text("Connection closed") + await websocket.close() + break + + websocket_state[websocket_id] = (state + 1) % 4 + except WebSocketDisconnect: + pass + + +@websocket_endpoint.on_connect +def websocket_on_connect(websocket): + return "Hello world, from ws" -@websocket_json.on("close") -def jsonws_close(): +@websocket_endpoint.on_close +def websocket_on_close(websocket): return "GoodBye world, from ws" -@websocket.on("connect") -def connect(): +# --- JSON WebSocket endpoint --- +@app.websocket("/web_socket_json") +async def json_websocket_endpoint(websocket): + try: + while True: + msg = await websocket.receive_text() + websocket_id = websocket.id + response = {"ws_id": websocket_id, "resp": "", "msg": msg} + global websocket_state + state = websocket_state[websocket_id] + + if state == 0: + response["resp"] = "Whaaat??" + elif state == 1: + response["resp"] = "Whooo??" + elif state == 2: + response["resp"] = "*chika* *chika* Slim Shady." + + websocket_state[websocket_id] = (state + 1) % 3 + await websocket.send_json(response) + except WebSocketDisconnect: + pass + + +@json_websocket_endpoint.on_connect +def json_websocket_on_connect(websocket): return "Hello world, from ws" -@websocket_json.on("connect") -def jsonws_connect(): - return "Hello world, from ws" +@json_websocket_endpoint.on_close +def json_websocket_on_close(websocket): + return "GoodBye world, from ws" -@websocket_di.on("connect") -async def di_message_connect(global_dependencies, router_dependencies): - return global_dependencies["GLOBAL_DEPENDENCY"] + " " + router_dependencies["ROUTER_DEPENDENCY"] +# --- WebSocket with dependency injection --- +@app.websocket("/web_socket_di") +async def di_websocket_endpoint(websocket, global_dependencies=None, router_dependencies=None): + try: + while True: + _ = await websocket.receive_text() + global_dep = global_dependencies.get("GLOBAL_DEPENDENCY", "MISSING GLOBAL") if global_dependencies else "MISSING GLOBAL" + router_dep = router_dependencies.get("ROUTER_DEPENDENCY", "MISSING ROUTER") if router_dependencies else "MISSING ROUTER" + await websocket.send_text(f"handler: {global_dep} {router_dep}") + except WebSocketDisconnect: + pass -@websocket_di.on("message") -async def di_message(): - # Test empty return - should not send anything - pass +@di_websocket_endpoint.on_connect +async def di_websocket_on_connect(websocket, global_dependencies=None, router_dependencies=None): + global_dep = global_dependencies.get("GLOBAL_DEPENDENCY") if global_dependencies else "MISSING GLOBAL" + router_dep = router_dependencies.get("ROUTER_DEPENDENCY") if router_dependencies else "MISSING ROUTER" + return f"connect: {global_dep} {router_dep}" -@websocket_di.on("close") -async def di_message_close(): - # Test empty return - should not send anything - pass +@di_websocket_endpoint.on_close +async def di_websocket_on_close(websocket, global_dependencies=None): + global_dep = global_dependencies.get("GLOBAL_DEPENDENCY") if global_dependencies else "MISSING GLOBAL" + return f"close: {global_dep}" -@websocket_empty_returns.on("connect") -async def empty_connect(): - """Test async handler with no return""" - # No return statement - should not send anything - pass +# --- WebSocket with empty returns --- +@app.websocket("/web_socket_empty_returns") +async def empty_websocket_endpoint(websocket): + try: + while True: + await websocket.receive_text() + # No response sent + except WebSocketDisconnect: + pass -@websocket_empty_returns.on("message") -def empty_message_sync(): - """Test sync handler with no return""" - # No return statement - should not send anything +@empty_websocket_endpoint.on_connect +async def empty_websocket_on_connect(websocket): + """Test async handler with no return""" pass -@websocket_empty_returns.on("close") -async def empty_close(): +@empty_websocket_endpoint.on_close +async def empty_websocket_on_close(websocket): """Test async handler with explicit None return""" return None diff --git a/integration_tests/test_web_sockets.py b/integration_tests/test_web_sockets.py index 0c47eef0f..ea62466cd 100644 --- a/integration_tests/test_web_sockets.py +++ b/integration_tests/test_web_sockets.py @@ -57,15 +57,22 @@ def test_web_socket_json(session): def test_websocket_di(session): - """ - Not using this as the benchmark test since this involves JSON marshalling/unmarshalling + """Test dependency injection in WebSocket connect and handler phases.""" - """ + ws = create_connection(f"{BASE_URL}/web_socket_di") - msg = "GLOBAL DEPENDENCY ROUTER DEPENDENCY" + # 1. on_connect should receive both global and router dependencies + assert ws.recv() == "connect: GLOBAL DEPENDENCY ROUTER DEPENDENCY" - ws = create_connection(f"{BASE_URL}/web_socket_di") - assert ws.recv() == msg + # 2. Main handler should also receive both dependencies when processing messages + ws.send("test") + assert ws.recv() == "handler: GLOBAL DEPENDENCY ROUTER DEPENDENCY" + + # Send another message to confirm DI is stable across multiple messages + ws.send("test again") + assert ws.recv() == "handler: GLOBAL DEPENDENCY ROUTER DEPENDENCY" + + ws.close() def test_websocket_empty_returns(session): diff --git a/robyn/__init__.py b/robyn/__init__.py index d67284a60..45aea5d5d 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -24,7 +24,7 @@ from robyn.robyn import FunctionInfo, Headers, HttpMethod, Request, Response, WebSocketConnector, get_version from robyn.router import MiddlewareRouter, MiddlewareType, Router, WebSocketRouter from robyn.types import Directory -from robyn.ws import WebSocket +from robyn.ws import WebSocket, WebSocketAdapter, WebSocketDisconnect, create_websocket_decorator __version__ = get_version() @@ -290,8 +290,29 @@ def exclude_response_headers_for(self, excluded_response_headers_paths: Optional """ self.excluded_response_headers_paths = excluded_response_headers_paths - def add_web_socket(self, endpoint: str, ws: WebSocket) -> None: - self.web_socket_router.add_route(endpoint, ws) + def add_web_socket(self, endpoint: str, handlers) -> None: + self.web_socket_router.add_route(endpoint, handlers) + + def websocket(self, endpoint: str): + """ + Modern WebSocket decorator backed by Rust channels. + + Usage: + @app.websocket("/ws") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") + + @handler.on_connect + def on_connect(websocket): + return "Welcome!" + + @handler.on_close + def on_close(websocket): + return "Goodbye" + """ + return create_websocket_decorator(self)(endpoint) def _add_event_handler(self, event_type: Events, handler: Callable) -> None: logger.info("Added event %s handler", event_type) @@ -554,10 +575,13 @@ def include_router(self, router: "SubRouter"): self.openapi.add_subrouter_paths(self.openapi) # extend the websocket routes - prefix = router.prefix - for route in router.web_socket_router.routes: - new_endpoint = f"{prefix}{route}" - self.web_socket_router.routes[new_endpoint] = router.web_socket_router.routes[route] + prefix = _normalize_endpoint(router.prefix, treat_empty_as_root=True) + if prefix == "/": + prefix = "" + for route, handlers in router.web_socket_router.routes.items(): + normalized_route = _normalize_endpoint(route) + new_endpoint = f"{prefix}{normalized_route}" + self.web_socket_router.routes[new_endpoint] = handlers self.dependencies.merge_dependencies(router) @@ -705,6 +729,12 @@ def trace(self, endpoint: str, auth_required: bool = False, openapi_name: str = def options(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["options"]): return super().options(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags) + def websocket(self, endpoint: str): + """ + Modern WebSocket decorator for SubRouter with prefix support. + """ + return create_websocket_decorator(self)(endpoint) + def ALLOW_CORS(app: Robyn, origins: Union[List[str], str], headers: Union[List[str], str] = None): """ @@ -778,5 +808,7 @@ def cors_middleware(request): "Headers", "WebSocketConnector", "WebSocket", + "WebSocketAdapter", + "WebSocketDisconnect", "MCPApp", ] diff --git a/robyn/processpool.py b/robyn/processpool.py index db656d94d..4b4efac1a 100644 --- a/robyn/processpool.py +++ b/robyn/processpool.py @@ -11,7 +11,6 @@ from robyn.robyn import FunctionInfo, Headers, Server, SocketHeld from robyn.router import GlobalMiddleware, Route, RouteMiddleware from robyn.types import Directory -from robyn.ws import WebSocket def run_processes( @@ -22,7 +21,7 @@ def run_processes( routes: List[Route], global_middlewares: List[GlobalMiddleware], route_middlewares: List[RouteMiddleware], - web_sockets: Dict[str, WebSocket], + web_sockets: Dict[str, dict], event_handlers: Dict[Events, FunctionInfo], workers: int, processes: int, @@ -76,7 +75,7 @@ def init_processpool( routes: List[Route], global_middlewares: List[GlobalMiddleware], route_middlewares: List[RouteMiddleware], - web_sockets: Dict[str, WebSocket], + web_sockets: Dict[str, dict], event_handlers: Dict[Events, FunctionInfo], socket: SocketHeld, workers: int, @@ -154,7 +153,7 @@ def spawn_process( routes: List[Route], global_middlewares: List[GlobalMiddleware], route_middlewares: List[RouteMiddleware], - web_sockets: Dict[str, WebSocket], + web_sockets: Dict[str, dict], event_handlers: Dict[Events, FunctionInfo], socket: SocketHeld, workers: int, @@ -211,11 +210,21 @@ def spawn_process( for endpoint in web_sockets: web_socket = web_sockets[endpoint] + # Support both old-style WebSocket objects and new-style handler dicts + if hasattr(web_socket, "methods"): + # Old-style: WebSocket class with .methods dict + methods = web_socket.methods + use_channel = False + else: + # New-style: plain dict of handlers + methods = web_socket + use_channel = web_socket.get("_use_channel", False) server.add_web_socket_route( endpoint, - web_socket.methods["connect"], - web_socket.methods["close"], - web_socket.methods["message"], + methods["connect"], + methods["close"], + methods["message"], + use_channel, ) try: diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi index 0714b345c..d9e128aff 100644 --- a/robyn/robyn.pyi +++ b/robyn/robyn.pyi @@ -499,6 +499,7 @@ class Server: connect_route: FunctionInfo, close_route: FunctionInfo, message_route: FunctionInfo, + use_channel: bool, ) -> None: pass def start(self, socket: SocketHeld, workers: int, client_timeout: int, keep_alive_timeout: int) -> None: diff --git a/robyn/router.py b/robyn/router.py index a8e11bc40..3bbf3666f 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -13,7 +13,6 @@ from robyn.responses import FileResponse, StreamingResponse from robyn.robyn import FunctionInfo, Headers, HttpMethod, Identity, MiddlewareType, QueryParams, Request, Response, Url from robyn.types import Body, Files, FormData, IPAddress, Method, PathParams -from robyn.ws import WebSocket _logger = logging.getLogger(__name__) @@ -46,7 +45,7 @@ class GlobalMiddleware(NamedTuple): class BaseRouter(ABC): @abstractmethod - def add_route(*args) -> Union[Callable, CoroutineType, WebSocket]: ... + def add_route(*args) -> Union[Callable, CoroutineType, Dict]: ... class Router(BaseRouter): @@ -417,10 +416,10 @@ def get_global_middlewares(self) -> List[GlobalMiddleware]: class WebSocketRouter(BaseRouter): def __init__(self) -> None: super().__init__() - self.routes: dict = {} + self.routes: Dict[str, dict] = {} - def add_route(self, endpoint: str, web_socket: WebSocket) -> None: # type: ignore - self.routes[endpoint] = web_socket + def add_route(self, endpoint: str, handlers: dict) -> None: # type: ignore + self.routes[endpoint] = handlers - def get_routes(self) -> Dict[str, WebSocket]: + def get_routes(self) -> Dict[str, dict]: return self.routes diff --git a/robyn/ws.py b/robyn/ws.py index a1b39d5fc..e85cfeba1 100644 --- a/robyn/ws.py +++ b/robyn/ws.py @@ -1,23 +1,284 @@ from __future__ import annotations +import asyncio import inspect -from typing import TYPE_CHECKING, Callable +import logging +from typing import TYPE_CHECKING, Callable, Dict + +import orjson from robyn.argument_parser import Config from robyn.dependency_injection import DependencyMap -from robyn.robyn import FunctionInfo +from robyn.robyn import FunctionInfo, WebSocketConnector if TYPE_CHECKING: from robyn import Robyn -import logging - _logger = logging.getLogger(__name__) +class WebSocketDisconnect(Exception): + """Exception raised when a WebSocket connection is disconnected.""" + + def __init__(self, code: int = 1000, reason: str = ""): + self.code = code + self.reason = reason + super().__init__(f"WebSocket disconnected with code {code}: {reason}") + + +class WebSocketAdapter: + """ + Modern WebSocket interface backed by Rust channels. + + Wraps a WebSocketConnector and a Rust WebSocketChannel to provide + a clean async API for WebSocket handlers. + """ + + def __init__(self, websocket_connector: WebSocketConnector, channel=None): + self._connector = websocket_connector + self._channel = channel + + async def receive_text(self) -> str: + """Receive the next text message. Blocks until a message arrives. + Raises WebSocketDisconnect when the connection is closed.""" + if self._channel is None: + raise WebSocketDisconnect(reason="No message channel available") + result = await self._channel.receive() + if result is None: + raise WebSocketDisconnect() + return result + + async def receive_bytes(self) -> bytes: + """Receive binary data (decoded from text).""" + text = await self.receive_text() + return text.encode("utf-8") + + async def receive_json(self): + """Receive and decode JSON data.""" + text = await self.receive_text() + if text is None: + return None + return orjson.loads(text) + + async def send_text(self, data: str): + """Send text data to this WebSocket client.""" + await self._connector.async_send_to(self._connector.id, data) + + async def send_bytes(self, data: bytes): + """Send binary data (as text) to this WebSocket client.""" + await self._connector.async_send_to(self._connector.id, data.decode("utf-8")) + + async def send_json(self, data): + """Send JSON data to this WebSocket client.""" + await self.send_text(orjson.dumps(data).decode()) + + async def broadcast(self, data: str): + """Broadcast text data to all connected WebSocket clients on this endpoint.""" + await self._connector.async_broadcast(data) + + async def close(self): + """Close the WebSocket connection.""" + self._connector.close() + + @property + def id(self) -> str: + """WebSocket connection ID.""" + return self._connector.id + + @property + def query_params(self): + """Access query parameters from the connection URL.""" + return self._connector.query_params + + +# Global storage for connection state (per-connection queues and tasks) +_connection_tasks: Dict[str, asyncio.Task] = {} + + +def create_websocket_decorator(app_instance): + """ + Factory function to create a websocket decorator for an app instance. + Returns a decorator that registers a modern WebSocket endpoint + backed by Rust channels. + """ + + def websocket(endpoint: str): + """ + Modern WebSocket decorator. + + Usage: + @app.websocket("/ws") + async def handler(websocket): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") + + @handler.on_connect + def on_connect(websocket): + return "Welcome!" + + @handler.on_close + def on_close(websocket): + return "Goodbye" + """ + + def decorator(handler): + _on_connect_fn = None + _on_close_fn = None + + def _get_di_kwargs(func): + """Build DI kwargs for a function based on its signature.""" + sig_params = dict(inspect.signature(func).parameters) + injected = app_instance.dependencies.get_dependency_map(app_instance) + kwargs = {} + if "global_dependencies" in sig_params: + kwargs["global_dependencies"] = injected.get("global_dependencies", {}) + if "router_dependencies" in sig_params: + kwargs["router_dependencies"] = injected.get("router_dependencies", {}) + return kwargs + + # --- Connect handler (called by Rust on connection open) --- + async def connect_handler(ws): + """Internal connect handler called by Rust. + Creates the adapter, starts the user's handler task, + and calls the user's on_connect callback.""" + conn_id = ws.id + channel = ws.message_channel + + # Create the adapter with the Rust channel + adapter = WebSocketAdapter(ws, channel) + + # Build DI kwargs for the main handler + di_kwargs = _get_di_kwargs(handler) + + # Start the user's handler as a long-running asyncio task + async def _run_handler(): + try: + await handler(adapter, **di_kwargs) + except WebSocketDisconnect: + pass + except Exception as e: + if "connection closed" in str(e).lower() or "websocket" in str(e).lower(): + pass + else: + _logger.exception("Error in WebSocket handler for %s: %s", endpoint, e) + finally: + _connection_tasks.pop(conn_id, None) + + task = asyncio.create_task(_run_handler()) + _connection_tasks[conn_id] = task + + # Call user's on_connect if defined + if _on_connect_fn is not None: + connect_adapter = WebSocketAdapter(ws, channel) + connect_di = _get_di_kwargs(_on_connect_fn) + if asyncio.iscoroutinefunction(_on_connect_fn): + result = await _on_connect_fn(connect_adapter, **connect_di) + else: + result = _on_connect_fn(connect_adapter, **connect_di) + return result + + return None + + # --- Message handler (dummy for new-style; Rust pushes to channel instead) --- + async def message_handler(ws, msg): + """Dummy message handler. In channel mode, Rust pushes messages + directly to the channel and never calls this.""" + return None + + # --- Close handler (called by Rust on connection close) --- + async def close_handler(ws): + """Internal close handler called by Rust. + Waits for the handler task to finish and calls on_close.""" + conn_id = ws.id + + # Wait for the handler task to finish (it should exit because + # the channel was closed by Rust, triggering WebSocketDisconnect) + task = _connection_tasks.pop(conn_id, None) + if task is not None: + try: + await asyncio.wait_for(task, timeout=5.0) + except (asyncio.TimeoutError, asyncio.CancelledError, Exception): + if not task.done(): + task.cancel() + + # Call user's on_close if defined + if _on_close_fn is not None: + close_adapter = WebSocketAdapter(ws, None) + close_di = _get_di_kwargs(_on_close_fn) + if asyncio.iscoroutinefunction(_on_close_fn): + result = await _on_close_fn(close_adapter, **close_di) + else: + result = _on_close_fn(close_adapter, **close_di) + return result + + return None + + # --- Build FunctionInfo objects for Rust --- + handlers = {} + + # Connect handler FunctionInfo + connect_params = dict(inspect.signature(connect_handler).parameters) + handlers["connect"] = FunctionInfo( + connect_handler, + True, # is_async + len(connect_params), + connect_params, + {}, # no kwargs needed - DI handled in Python + ) + + # Message handler FunctionInfo (dummy, won't be called in channel mode) + message_params = dict(inspect.signature(message_handler).parameters) + handlers["message"] = FunctionInfo( + message_handler, + True, + len(message_params), + message_params, + {}, + ) + + # Close handler FunctionInfo + close_params = dict(inspect.signature(close_handler).parameters) + handlers["close"] = FunctionInfo( + close_handler, + True, + len(close_params), + close_params, + {}, + ) + + # Mark as channel-based + handlers["_use_channel"] = True + + # --- Decorator methods for on_connect / on_close --- + def add_on_connect(connect_fn): + nonlocal _on_connect_fn + _on_connect_fn = connect_fn + return connect_fn + + def add_on_close(close_fn): + nonlocal _on_close_fn + _on_close_fn = close_fn + return close_fn + + handler.on_connect = add_on_connect + handler.on_close = add_on_close + + # Register with the app + app_instance.add_web_socket(endpoint, handlers) + + return handler + + return decorator + + return websocket + + class WebSocket: - # should this be websocket router? - """This is the python wrapper for the web socket that will be used here.""" + """Legacy WebSocket class for backward compatibility. + + Uses the old event-based API with @websocket.on("connect"/"message"/"close"). + """ def __init__(self, robyn_object: "Robyn", endpoint: str, config: Config = Config(), dependencies: DependencyMap = DependencyMap()) -> None: self.robyn_object = robyn_object @@ -38,14 +299,15 @@ def inner(handler): injected_dependencies = self.dependencies.get_dependency_map(self) new_injected_dependencies = {} - for dependency in injected_dependencies: - if dependency in params: - new_injected_dependencies[dependency] = injected_dependencies[dependency] - else: - _logger.debug(f"Dependency {dependency} is not used in the handler {handler.__name__}") - - self.methods[type] = FunctionInfo(handler, is_async, num_params, params, kwargs=new_injected_dependencies) - self.robyn_object.add_web_socket(self.endpoint, self) + if "global_dependencies" in params: + new_injected_dependencies["global_dependencies"] = injected_dependencies.get("global_dependencies", {}) + if "router_dependencies" in params: + new_injected_dependencies["router_dependencies"] = injected_dependencies.get("router_dependencies", {}) + + self.methods[type] = FunctionInfo(handler, is_async, num_params, params, new_injected_dependencies) + self.robyn_object.add_web_socket(self.endpoint, self) + + return handler return inner diff --git a/src/lib.rs b/src/lib.rs index 3bf2e795b..4ed95de84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ use types::{ HttpMethod, Url, }; -use websockets::{registry::WebSocketRegistry, WebSocketConnector}; +use websockets::{registry::WebSocketRegistry, WebSocketChannel, WebSocketConnector}; #[pyfunction] fn get_version() -> String { @@ -41,6 +41,7 @@ pub fn robyn(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/routers/web_socket_router.rs b/src/routers/web_socket_router.rs index af9f80704..8ac3040d7 100644 --- a/src/routers/web_socket_router.rs +++ b/src/routers/web_socket_router.rs @@ -7,15 +7,19 @@ use crate::types::function_info::FunctionInfo; /// Contains the thread safe hashmaps of different routes type WebSocketRoutes = RwLock>>; +/// Tracks which endpoints use the new channel-based message delivery +type WebSocketChannelFlags = RwLock>; pub struct WebSocketRouter { web_socket_routes: WebSocketRoutes, + channel_flags: WebSocketChannelFlags, } impl WebSocketRouter { pub fn new() -> Self { Self { web_socket_routes: RwLock::new(HashMap::new()), + channel_flags: RwLock::new(HashMap::new()), } } @@ -24,6 +28,11 @@ impl WebSocketRouter { &self.web_socket_routes } + #[inline] + pub fn get_channel_flags(&self) -> &WebSocketChannelFlags { + &self.channel_flags + } + // Checks if the functions is an async function // Inserts them in the router according to their nature(CoRoutine/SyncFunction) pub fn add_websocket_route( @@ -32,6 +41,7 @@ impl WebSocketRouter { connect_route: FunctionInfo, close_route: FunctionInfo, message_route: FunctionInfo, + use_channel: bool, ) { let table = self.get_web_socket_map(); @@ -48,5 +58,9 @@ impl WebSocketRouter { insert_in_router(connect_route, "connect"); insert_in_router(close_route, "close"); insert_in_router(message_route, "message"); + + self.channel_flags + .write() + .insert(route.to_string(), use_channel); } } diff --git a/src/server.rs b/src/server.rs index 6aab757c4..324257ba0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -193,10 +193,12 @@ impl Server { .app_data(web::Data::new(excluded_response_headers_paths.clone())); let web_socket_map = web_socket_router.get_web_socket_map(); + let channel_flags = web_socket_router.get_channel_flags(); for (elem, value) in (web_socket_map.read()).iter() { let endpoint = elem.clone(); let path_params = value.clone(); let endpoint_for_closure = endpoint.clone(); + let use_channel = *channel_flags.read().get(&endpoint).unwrap_or(&false); app = app.route( &endpoint, web::get().to(move |stream: web::Payload, req: HttpRequest| { @@ -209,6 +211,7 @@ impl Server { path_params.clone(), task_locals, endpoint_copy.to_string(), + use_channel, ) }), ); @@ -443,9 +446,15 @@ impl Server { connect_route: FunctionInfo, close_route: FunctionInfo, message_route: FunctionInfo, + use_channel: bool, ) { - self.websocket_router - .add_websocket_route(route, connect_route, close_route, message_route); + self.websocket_router.add_websocket_route( + route, + connect_route, + close_route, + message_route, + use_channel, + ); } /// Add a new startup handler diff --git a/src/websockets/mod.rs b/src/websockets/mod.rs index a593fb732..21b42f577 100644 --- a/src/websockets/mod.rs +++ b/src/websockets/mod.rs @@ -15,11 +15,37 @@ use parking_lot::RwLock; use pyo3::prelude::*; use pyo3::IntoPyObject; use pyo3_async_runtimes::TaskLocals; +use std::sync::Arc; +use tokio::sync::mpsc; use uuid::Uuid; use registry::{Register, WebSocketRegistry}; use std::collections::HashMap; +/// A Rust-backed channel receiver exposed to Python. +/// Python handlers call `await channel.receive()` to get the next message. +/// Returns the message string, or None when the connection is closed. +#[pyclass] +pub struct WebSocketChannel { + receiver: Arc>>>, +} + +#[pymethods] +impl WebSocketChannel { + /// Await the next message from the WebSocket. + /// Returns the message string, or None if the connection was closed. + fn receive<'py>(&self, py: Python<'py>) -> PyResult> { + let receiver = self.receiver.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let mut rx = receiver.lock().await; + match rx.recv().await { + Some(Some(msg)) => Ok(Some(msg)), + Some(None) | None => Ok(None), + } + }) + } +} + /// Define HTTP actor #[pyclass] pub struct WebSocketConnector { @@ -28,6 +54,12 @@ pub struct WebSocketConnector { pub task_locals: TaskLocals, pub registry_addr: Addr, pub query_params: QueryParams, + /// Whether this connection uses the new channel-based message delivery. + pub use_channel: bool, + /// Sender side of the message channel (stays in the Actix actor). + pub message_sender: Option>>, + /// Receiver side exposed to Python via WebSocketChannel. + pub message_channel: Option>, } // By default mailbox capacity is 16 messages. @@ -42,6 +74,23 @@ impl Actor for WebSocketConnector { addr: addr.clone(), }); + // If new-style (channel mode), create the tokio channel + if self.use_channel { + let (tx, rx) = mpsc::unbounded_channel::>(); + self.message_sender = Some(tx); + self.message_channel = Python::with_gil(|py| { + Some( + Py::new( + py, + WebSocketChannel { + receiver: Arc::new(tokio::sync::Mutex::new(rx)), + }, + ) + .unwrap(), + ) + }); + } + let function = self.router.get("connect").unwrap(); execute_ws_function(function, None, &self.task_locals, ctx, self); @@ -49,6 +98,11 @@ impl Actor for WebSocketConnector { } fn stopped(&mut self, ctx: &mut Self::Context) { + // Drop the sender to close the channel. + // This causes any pending `channel.receive()` in Python to return None, + // which the WebSocketAdapter converts to WebSocketDisconnect. + self.message_sender.take(); + let function = self.router.get("close").unwrap(); execute_ws_function(function, None, &self.task_locals, ctx, self); debug!("Actor is dead"); @@ -65,6 +119,11 @@ impl Clone for WebSocketConnector { task_locals: task_locals_clone, registry_addr: self.registry_addr.clone(), query_params: self.query_params.clone(), + use_channel: self.use_channel, + message_sender: self.message_sender.clone(), + message_channel: Python::with_gil(|py| { + self.message_channel.as_ref().map(|c| c.clone_ref(py)) + }), } } } @@ -89,29 +148,33 @@ impl StreamHandler> for WebSocketConnecto match msg { Ok(ws::Message::Ping(msg)) => { debug!("Ping message {:?}", msg); - let function = self.router.get("connect").unwrap(); - debug!("{:?}", function.handler); - execute_ws_function(function, None, &self.task_locals, ctx, self); ctx.pong(&msg) } Ok(ws::Message::Pong(msg)) => { debug!("Pong message {:?}", msg); } Ok(ws::Message::Text(text)) => { - // need to also pass this text as a param debug!("Text message received {:?}", text); - let function = self.router.get("message").unwrap(); - execute_ws_function( - function, - Some(text.to_string()), - &self.task_locals, - ctx, - self, - ); + if let Some(ref sender) = self.message_sender { + // New-style: push to Rust channel. No GIL. No Python call. + let _ = sender.send(Some(text.to_string())); + } else { + // Old-style: call Python message handler directly + let function = self.router.get("message").unwrap(); + execute_ws_function( + function, + Some(text.to_string()), + &self.task_locals, + ctx, + self, + ); + } } Ok(ws::Message::Binary(bin)) => ctx.binary(bin), Ok(ws::Message::Close(_close_reason)) => { debug!("Socket was closed"); + // Drop sender to signal channel closure + self.message_sender.take(); let function = self.router.get("close").unwrap(); execute_ws_function(function, None, &self.task_locals, ctx, self); } @@ -199,6 +262,13 @@ impl WebSocketConnector { pub fn get_query_params(&self) -> QueryParams { self.query_params.clone() } + + /// Get the message channel for new-style WebSocket handlers. + /// Returns None for old-style handlers. + #[getter] + pub fn get_message_channel(&self, py: Python) -> Option> { + self.message_channel.as_ref().map(|c| c.clone_ref(py)) + } } static REGISTRY_ADDRESSES: OnceCell>>> = @@ -230,6 +300,7 @@ pub async fn start_web_socket( router: HashMap, task_locals: TaskLocals, endpoint: String, + use_channel: bool, ) -> Result { let registry_addr = get_or_init_registry_for_endpoint(endpoint); @@ -253,6 +324,9 @@ pub async fn start_web_socket( id: Uuid::new_v4(), registry_addr, query_params, + use_channel, + message_sender: None, + message_channel: None, }, &req, stream, From 0472e4272f480f053a787f238cfe7cac8d0bd385 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sat, 14 Feb 2026 16:20:24 +0000 Subject: [PATCH 034/106] Release 0.78.0 --- .github/workflows/release-CI.yml | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- docs_src/public/llms.txt | 2 +- llms.txt | 2 +- pyproject.toml | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-CI.yml b/.github/workflows/release-CI.yml index d26f1c7cb..a2612d037 100644 --- a/.github/workflows/release-CI.yml +++ b/.github/workflows/release-CI.yml @@ -120,7 +120,6 @@ jobs: { version: "3.11", abi: "cp311-cp311" }, { version: "3.12", abi: "cp312-cp312" }, { version: "3.13", abi: "cp313-cp313" }, - { version: "3.14", abi: "cp314-cp314" }, ] target: [aarch64, armv7] steps: @@ -132,6 +131,7 @@ jobs: with: target: ${{ matrix.target }} manylinux: auto + maturin-version: "v1.12.0" args: -i python${{matrix.python.version}} --release --out dist - uses: uraimo/run-on-arch-action@v2 name: Install build wheel diff --git a/Cargo.lock b/Cargo.lock index ae7801fd4..91c17a350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.77.0" +version = "0.78.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 74b912d9c..c496c0437 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.77.0" +version = "0.78.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/docs_src/public/llms.txt b/docs_src/public/llms.txt index 93b40a43d..19153884b 100644 --- a/docs_src/public/llms.txt +++ b/docs_src/public/llms.txt @@ -4,7 +4,7 @@ ## Quick Facts -- Version: 0.77.0 +- Version: 0.78.0 - Python: >= 3.10 - License: BSD 2.0 - Repository: https://github.com/sparckles/robyn diff --git a/llms.txt b/llms.txt index 93b40a43d..19153884b 100644 --- a/llms.txt +++ b/llms.txt @@ -4,7 +4,7 @@ ## Quick Facts -- Version: 0.77.0 +- Version: 0.78.0 - Python: >= 3.10 - License: BSD 2.0 - Repository: https://github.com/sparckles/robyn diff --git a/pyproject.toml b/pyproject.toml index 2ecf38e62..58c5ce3ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.77.0" +version = "0.78.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -67,7 +67,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.77.0" +version = "0.78.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 497c256ff99d2a019fd9d3e1e9e657d68f9d95f5 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 15 Feb 2026 19:55:37 +0000 Subject: [PATCH 035/106] fix: update next-mdx-remote to v6.0.0 to address CVE-2026-0969 (#1312) Vulnerable version (4.4.1) allowed arbitrary code execution when rendering untrusted MDX content. v6.0.0 introduces blockJS and blockDangerousJS parameters (defaulting to true) for security. Co-authored-by: Cursor --- docs_src/package-lock.json | 10651 +++++++++++++++++++++++++---------- docs_src/package.json | 2 +- 2 files changed, 7825 insertions(+), 2828 deletions(-) diff --git a/docs_src/package-lock.json b/docs_src/package-lock.json index 1bb60e905..fa0f77ea7 100644 --- a/docs_src/package-lock.json +++ b/docs_src/package-lock.json @@ -31,7 +31,7 @@ "mdx-annotations": "^0.1.3", "meilisearch": "^0.33.0", "next": "13.4.2", - "next-mdx-remote": "^4.4.1", + "next-mdx-remote": "^6.0.0", "next-router-mock": "^0.9.3", "postcss-focus-visible": "^6.0.4", "prism-themes": "^1.9.0", @@ -214,6 +214,27 @@ "@algolia/requester-common": "4.17.2" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/runtime": { "version": "7.17.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", @@ -853,11 +874,6 @@ "@types/unist": "*" } }, - "node_modules/@types/js-yaml": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", - "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==" - }, "node_modules/@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -1028,6 +1044,11 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, "node_modules/@vercel/analytics": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.2.tgz", @@ -1349,7 +1370,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { "version": "4.2.2", @@ -2039,6 +2061,15 @@ "node": ">=6" } }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2306,6 +2337,18 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2510,6 +2553,96 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-estree/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/esast-util-from-estree/node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-estree/node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/esast-util-from-js/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3073,6 +3206,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-util-to-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz", @@ -3887,2088 +4033,2452 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-string": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-1.0.4.tgz", - "integrity": "sha512-eK0MxRX47AV2eZ+Lyr18DCpQgodvaS3fAQO2+b9Two9F5HEoRPhiUMNzoXArMJfZi2yieFzUBMRl3HNJ3Jus3w==", + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", - "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==", + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "node_modules/hast-util-to-jsx-runtime/node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" + "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "node_modules/hast-util-to-jsx-runtime/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "node_modules/hast-util-to-jsx-runtime/node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "dependencies": { - "xtend": "^4.0.0" + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "node_modules/hast-util-to-jsx-runtime/node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/highlight.js": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", - "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", - "engines": { - "node": ">=12.0.0" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "node_modules/hast-util-to-jsx-runtime/node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "dependencies": { - "parse-passwd": "^1.0.0" + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/hast-util-to-jsx-runtime/node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, + "node_modules/hast-util-to-jsx-runtime/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" + "@types/mdast": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", - "engines": { - "node": ">=4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "engines": { - "node": ">=0.8.19" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" - }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-alphabetical": { + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-factory-space": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-alphanumerical": { + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-factory-title": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/feross" + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" }, { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "OpenCollective", + "url": "https://opencollective.com/unified" } ], - "engines": { - "node": ">=4" + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/is-decimal": { + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-encode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "engines": { - "node": ">=0.10.0" - } + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "engines": { - "node": ">=0.10.0" + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "engines": { - "node": ">=0.10.0" + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-hexadecimal": { + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "node_modules/hast-util-to-jsx-runtime/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "dependencies": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - }, - "engines": { - "node": ">=8" + "@types/unist": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" + "node_modules/hast-util-to-jsx-runtime/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-npm": { + "node_modules/hast-util-to-jsx-runtime/node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "engines": { - "node": ">=8" + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" + "node_modules/hast-util-to-jsx-runtime/node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, + "node_modules/hast-util-to-jsx-runtime/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dependencies": { - "has-tostringtag": "^1.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, - "engines": { - "node": ">= 0.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "engines": { - "node": ">=8" + "node_modules/hast-util-to-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-1.0.4.tgz", + "integrity": "sha512-eK0MxRX47AV2eZ+Lyr18DCpQgodvaS3fAQO2+b9Two9F5HEoRPhiUMNzoXArMJfZi2yieFzUBMRl3HNJ3Jus3w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" + "node_modules/hast-util-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", + "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", - "engines": { - "node": ">=0.10.0" + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-reference": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.0.tgz", - "integrity": "sha512-Eo1W3wUoHWoCoVM4GVl/a+K0IgiqE5aIo4kJABFyMum1ZORlPkC+UC357sSQUL5w5QCE5kCC9upl75b7+7CY/Q==", + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", "dependencies": { - "@types/estree": "*" + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "node_modules/highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dependencies": { - "call-bind": "^1.0.2" + "parse-passwd": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 4" } }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", + "engines": { + "node": ">=4" + } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "peer": true, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" }, "engines": { - "node": ">= 10.13.0" + "node": ">=8.0.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "peer": true, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">= 0.4" } }, - "node_modules/jiti": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", - "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", - "bin": { - "jiti": "bin/jiti.js" + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/js-sdsl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", - "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "dependencies": { - "argparse": "^2.0.1" + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, "dependencies": { - "minimist": "^1.2.0" + "has-bigints": "^1.0.1" }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", - "integrity": "sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==", - "dev": true, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": { - "array-includes": "^3.1.4", - "object.assign": "^4.1.2" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=4.0" + "node": ">=8" } }, - "node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "dependencies": { - "json-buffer": "3.0.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", - "dev": true - }, - "node_modules/language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, - "dependencies": { - "language-subtag-registry": "~0.3.2" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", "dependencies": { - "package-json": "^6.3.0" + "ci-info": "^2.0.0" }, - "engines": { - "node": ">=8" + "bin": { + "is-ci": "bin.js" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "has": "^1.0.3" }, - "engines": { - "node": ">= 0.8.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/libnpx/-/libnpx-10.2.4.tgz", - "integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==", + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { - "dotenv": "^5.0.1", - "npm-package-arg": "^6.0.0", - "rimraf": "^2.6.2", - "safe-buffer": "^5.1.0", - "update-notifier": "^2.3.0", - "which": "^1.3.0", - "y18n": "^4.0.0", - "yargs": "^14.2.3" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx/node_modules/ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha512-TdlOggdA/zURfMYa7ABC66j+oqfMew58KpJMbUlH3bcZP1b+cBHIHDDn5uH9INsxrHBPjsqM0tDB4jPTF/vgJA==", - "dependencies": { - "string-width": "^2.0.0" + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/libnpx/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/libnpx/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/libnpx/node_modules/boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dependencies": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/libnpx/node_modules/camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/libnpx/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/libnpx/node_modules/ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" - }, - "node_modules/libnpx/node_modules/cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==", - "engines": { - "node": ">=0.10.0" + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/libnpx/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", "dependencies": { - "color-name": "1.1.3" + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/libnpx/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/libnpx/node_modules/configstore": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", - "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", - "dependencies": { - "dot-prop": "^4.2.1", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/libnpx/node_modules/crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "engines": { - "node": ">=4" + "node": ">=0.12.0" } }, - "node_modules/libnpx/node_modules/dot-prop": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", - "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { - "is-obj": "^1.0.0" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "engines": { - "node": ">=0.8.0" + "node": ">=8" } }, - "node_modules/libnpx/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/libnpx/node_modules/global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", - "dependencies": { - "ini": "^1.3.4" - }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/libnpx/node_modules/got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==", - "dependencies": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - }, + "node_modules/is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/libnpx/node_modules/has-flag": { + "node_modules/is-reference": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.0.tgz", + "integrity": "sha512-Eo1W3wUoHWoCoVM4GVl/a+K0IgiqE5aIo4kJABFyMum1ZORlPkC+UC357sSQUL5w5QCE5kCC9upl75b7+7CY/Q==", + "dependencies": { + "@types/estree": "*" } }, - "node_modules/libnpx/node_modules/is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { - "ci-info": "^1.5.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, - "bin": { - "is-ci": "bin.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/libnpx/node_modules/is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "dependencies": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" + "call-bind": "^1.0.2" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx/node_modules/is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==", + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "engines": { "node": ">=0.10.0" } }, - "node_modules/libnpx/node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx/node_modules/is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==", + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { - "path-is-inside": "^1.0.1" + "has-symbols": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx/node_modules/latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==", + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "dependencies": { - "package-json": "^4.0.0" + "call-bind": "^1.0.2" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/libnpx/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "peer": true, "dependencies": { - "pify": "^3.0.0" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">=4" + "node": ">= 10.13.0" } }, - "node_modules/libnpx/node_modules/package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha512-q/R5GrMek0vzgoomq6rm9OX+3PQve8sLwTirmK30YB3Cu0Bbt9OX9M/SIUnroN5BGJkzwGsFwDaRGD9EwBOlCA==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, "dependencies": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/libnpx/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "engines": { - "node": ">=4" + "node_modules/jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "bin": { + "jiti": "bin/jiti.js" } }, - "node_modules/libnpx/node_modules/prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", - "engines": { - "node": ">=0.10.0" - } + "node_modules/js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true }, - "node_modules/libnpx/node_modules/registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "dependencies": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "node_modules/libnpx/node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { - "rc": "^1.0.1" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/libnpx/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, "dependencies": { - "glob": "^7.1.3" + "minimist": "^1.2.0" }, "bin": { - "rimraf": "bin.js" + "json5": "lib/cli.js" } }, - "node_modules/libnpx/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, - "node_modules/libnpx/node_modules/semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", - "dependencies": { - "semver": "^5.0.3" - }, - "engines": { - "node": ">=0.10.0" + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/libnpx/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "node_modules/jsx-ast-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", + "integrity": "sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==", + "dev": true, "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "array-includes": "^3.1.4", + "object.assign": "^4.1.2" }, "engines": { - "node": ">=4" + "node": ">=4.0" } }, - "node_modules/libnpx/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" + "json-buffer": "3.0.0" } }, - "node_modules/libnpx/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/libnpx/node_modules/term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==", - "dependencies": { - "execa": "^0.7.0" - }, - "engines": { - "node": ">=4" - } + "node_modules/language-subtag-registry": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", + "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", + "dev": true }, - "node_modules/libnpx/node_modules/unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dev": true, "dependencies": { - "crypto-random-string": "^1.0.0" - }, - "engines": { - "node": ">=4" + "language-subtag-registry": "~0.3.2" } }, - "node_modules/libnpx/node_modules/update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", "dependencies": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" + "package-json": "^6.3.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/libnpx/node_modules/url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { - "prepend-http": "^1.0.1" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/libnpx/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node": ">= 0.8.0" } }, - "node_modules/libnpx/node_modules/widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "node_modules/libnpx": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/libnpx/-/libnpx-10.2.4.tgz", + "integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==", "dependencies": { - "string-width": "^2.1.1" + "dotenv": "^5.0.1", + "npm-package-arg": "^6.0.0", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.0", + "update-notifier": "^2.3.0", + "which": "^1.3.0", + "y18n": "^4.0.0", + "yargs": "^14.2.3" }, "engines": { "node": ">=4" } }, - "node_modules/libnpx/node_modules/write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "node_modules/libnpx/node_modules/ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha512-TdlOggdA/zURfMYa7ABC66j+oqfMew58KpJMbUlH3bcZP1b+cBHIHDDn5uH9INsxrHBPjsqM0tDB4jPTF/vgJA==", "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "string-width": "^2.0.0" } }, - "node_modules/libnpx/node_modules/xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", + "node_modules/libnpx/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "engines": { "node": ">=4" } }, - "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "node_modules/libnpx/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, "engines": { - "node": ">=10" + "node": ">=4" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/listify": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/listify/-/listify-1.0.3.tgz", - "integrity": "sha512-083swF7iH7bx8666zdzBColpgEuy46HjN3r1isD4zV6Ix7FuHfb/2/WVnl4CH8hjuoWeFF7P5KkKNXUnJCFEJg==", - "engines": { - "node": ">= 0.4" + "node_modules/libnpx/node_modules/boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dependencies": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "peer": true, + "node_modules/libnpx/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", "engines": { - "node": ">=6.11.5" + "node": ">=4" } }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, + "node_modules/libnpx/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.castarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "node_modules/libnpx/node_modules/ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" }, - "node_modules/longest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", - "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", + "node_modules/libnpx/node_modules/cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==", "engines": { "node": ">=0.10.0" } }, - "node_modules/longest-streak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", - "integrity": "sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/libnpx/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "node_modules/libnpx/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/libnpx/node_modules/configstore": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", + "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "dot-prop": "^4.2.1", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" }, - "bin": { - "loose-envify": "cli.js" + "engines": { + "node": ">=4" } }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "node_modules/libnpx/node_modules/crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, + "node_modules/libnpx/node_modules/dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", "dependencies": { - "yallist": "^4.0.0" + "is-obj": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">=4" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, + "node_modules/libnpx/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" + "node": ">=0.8.0" } }, - "node_modules/markdown-extensions": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", - "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==", + "node_modules/libnpx/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/markdown-table": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.2.tgz", - "integrity": "sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node": ">=4" } }, - "node_modules/mdast-util-definitions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz", - "integrity": "sha512-rQ+Gv7mHttxHOBx2dkF4HWTg+EE+UR78ptQWDylzPKaQuVGdG4HIoY3SrS/pCp80nZ04greFvXbVFHT+uf0JVQ==", + "node_modules/libnpx/node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" + "ini": "^1.3.4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-find-and-replace": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.1.tgz", - "integrity": "sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw==", + "node_modules/libnpx/node_modules/got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==", "dependencies": { - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/libnpx/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/mdast-util-from-markdown": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz", - "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==", + "node_modules/libnpx/node_modules/is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "ci-info": "^1.5.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "bin": { + "is-ci": "bin.js" } }, - "node_modules/mdast-util-gfm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.1.tgz", - "integrity": "sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ==", - "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-gfm-autolink-literal": "^1.0.0", - "mdast-util-gfm-footnote": "^1.0.0", - "mdast-util-gfm-strikethrough": "^1.0.0", - "mdast-util-gfm-table": "^1.0.0", - "mdast-util-gfm-task-list-item": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/libnpx/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.2.tgz", - "integrity": "sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==", + "node_modules/libnpx/node_modules/is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==", "dependencies": { - "@types/mdast": "^3.0.0", - "ccount": "^2.0.0", - "mdast-util-find-and-replace": "^2.0.0", - "micromark-util-character": "^1.0.0" + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-gfm-footnote": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.1.tgz", - "integrity": "sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0", - "micromark-util-normalize-identifier": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/libnpx/node_modules/is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/mdast-util-gfm-strikethrough": { + "node_modules/libnpx/node_modules/is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.1.tgz", - "integrity": "sha512-zKJbEPe+JP6EUv0mZ0tQUyLQOC+FADt0bARldONot/nefuISkaZFlmVK4tU6JgfyZGrky02m/I6PmehgAgZgqg==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/mdast-util-gfm-table": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.4.tgz", - "integrity": "sha512-aEuoPwZyP4iIMkf2cLWXxx3EQ6Bmh2yKy9MVCg4i6Sd3cX80dcLEfXO/V4ul3pGH9czBK4kp+FAl+ZHmSUt9/w==", + "node_modules/libnpx/node_modules/is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==", "dependencies": { - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.3.0" + "path-is-inside": "^1.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.1.tgz", - "integrity": "sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA==", + "node_modules/libnpx/node_modules/latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" + "package-json": "^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-mdx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.0.tgz", - "integrity": "sha512-M09lW0CcBT1VrJUaF/PYxemxxHa7SLDHdSn94Q9FhxjCQfuW7nMAWKWimTmA3OyDMSTH981NN1csW1X+HPSluw==", + "node_modules/libnpx/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dependencies": { - "mdast-util-mdx-expression": "^1.0.0", - "mdast-util-mdx-jsx": "^2.0.0", - "mdast-util-mdxjs-esm": "^1.0.0" + "pify": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-mdx-expression": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.0.tgz", - "integrity": "sha512-9kTO13HaL/ChfzVCIEfDRdp1m5hsvsm6+R8yr67mH+KS2ikzZ0ISGLPTbTswOFpLLlgVHO9id3cul4ajutCvCA==", + "node_modules/libnpx/node_modules/package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha512-q/R5GrMek0vzgoomq6rm9OX+3PQve8sLwTirmK30YB3Cu0Bbt9OX9M/SIUnroN5BGJkzwGsFwDaRGD9EwBOlCA==", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-mdx-jsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.0.tgz", - "integrity": "sha512-KzgzfWMhdteDkrY4mQtyvTU5bc/W4ppxhe9SzelO6QUUiwLAM+Et2Dnjjprik74a336kHdo0zKm7Tp+n6FFeRg==", + "node_modules/libnpx/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/libnpx/node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/libnpx/node_modules/registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "ccount": "^2.0.0", - "mdast-util-to-markdown": "^1.3.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-remove-position": "^4.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/libnpx/node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dependencies": { + "rc": "^1.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.0.tgz", - "integrity": "sha512-7N5ihsOkAEGjFotIX9p/YPdl4TqUoMxL4ajNz7PbT89BqsdWJuBC9rvgt6wpbwTZqWWR0jKWqQbwsOWDBUZv4g==", + "node_modules/libnpx/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" + "glob": "^7.1.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "bin": { + "rimraf": "bin.js" } }, - "node_modules/mdast-util-to-hast": { - "version": "12.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.1.2.tgz", - "integrity": "sha512-Wn6Mcj04qU4qUXHnHpPATYMH2Jd8RlntdnloDfYLe1ErWRHo6+pvSl/DzHp6sCZ9cBSYlc8Sk8pbwb8xtUoQhQ==", + "node_modules/libnpx/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/libnpx/node_modules/semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "@types/mdurl": "^1.0.0", - "mdast-util-definitions": "^5.0.0", - "mdurl": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "trim-lines": "^3.0.0", - "unist-builder": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" + "semver": "^5.0.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/mdast-util-to-markdown": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz", - "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==", + "node_modules/libnpx/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-to-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", - "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/libnpx/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "node_modules/libnpx/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/mdx-annotations": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/mdx-annotations/-/mdx-annotations-0.1.3.tgz", - "integrity": "sha512-2XrOlQeBDUa8GirNHy/Y7BR1h/P+vzk+1G2rzAfqJ+lg6JcEOKMCqsVkpVFSw0hdzpVuj9BnoNeA+aU1XPTkoA==", + "node_modules/libnpx/node_modules/term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==", "dependencies": { - "acorn": "^8.8.1", - "estree-util-visit": "^1.2.0", - "unist-util-visit": "^4.1.1" + "execa": "^0.7.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/meilisearch": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.33.0.tgz", - "integrity": "sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==", + "node_modules/libnpx/node_modules/unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", "dependencies": { - "cross-fetch": "^3.1.6" + "crypto-random-string": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "peer": true + "node_modules/libnpx/node_modules/update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dependencies": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/libnpx/node_modules/url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "dependencies": { + "prepend-http": "^1.0.1" + }, "engines": { - "node": ">= 8" + "node": ">=0.10.0" } }, - "node_modules/micromark": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz", - "integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/libnpx/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "node_modules/micromark-core-commonmark": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", - "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/libnpx/node_modules/widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=4" } }, - "node_modules/micromark-extension-gfm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz", - "integrity": "sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==", + "node_modules/libnpx/node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", "dependencies": { - "micromark-extension-gfm-autolink-literal": "^1.0.0", - "micromark-extension-gfm-footnote": "^1.0.0", - "micromark-extension-gfm-strikethrough": "^1.0.0", - "micromark-extension-gfm-table": "^1.0.0", - "micromark-extension-gfm-tagfilter": "^1.0.0", - "micromark-extension-gfm-task-list-item": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" } }, - "node_modules/micromark-extension-gfm-autolink-literal": { + "node_modules/libnpx/node_modules/xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/listify": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz", - "integrity": "sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "resolved": "https://registry.npmjs.org/listify/-/listify-1.0.3.tgz", + "integrity": "sha512-083swF7iH7bx8666zdzBColpgEuy46HjN3r1isD4zV6Ix7FuHfb/2/WVnl4CH8hjuoWeFF7P5KkKNXUnJCFEJg==", + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.0.4.tgz", - "integrity": "sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg==", - "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "peer": true, + "engines": { + "node": ">=6.11.5" } }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.4.tgz", - "integrity": "sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ==", + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/longest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", + "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/longest-streak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", + "integrity": "sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/micromark-extension-gfm-table": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz", - "integrity": "sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/micromark-extension-gfm-tagfilter": { + "node_modules/lowercase-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.1.tgz", - "integrity": "sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA==", - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.3.tgz", - "integrity": "sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q==", + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "yallist": "^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=10" } }, - "node_modules/micromark-extension-mdx-expression": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.3.tgz", - "integrity": "sha512-TjYtjEMszWze51NJCZmhv7MEBcgYRgb3tJeMAJ+HQCAaZHHRBaDCccqQzGizR/H4ODefP44wRTgOn2vE5I6nZA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dependencies": { - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.3.tgz", - "integrity": "sha512-VfA369RdqUISF0qGgv2FfV7gGjHDfn9+Qfiv5hEwpyr1xscRj/CiVRkU7rywGFCO7JwJ5L0e7CJz60lY52+qOA==", + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/markdown-extensions": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", + "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.2.tgz", + "integrity": "sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz", + "integrity": "sha512-rQ+Gv7mHttxHOBx2dkF4HWTg+EE+UR78ptQWDylzPKaQuVGdG4HIoY3SrS/pCp80nZ04greFvXbVFHT+uf0JVQ==", "dependencies": { - "@types/acorn": "^4.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-extension-mdx-md": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.0.tgz", - "integrity": "sha512-xaRAMoSkKdqZXDAoSgp20Azm0aRQKGOl0RrS81yGu8Hr/JhMsBmfs4wR7m9kgVUIO36cMUQjNyiyDKPrsv8gOw==", + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.1.tgz", + "integrity": "sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw==", "dependencies": { - "micromark-util-types": "^1.0.0" + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-extension-mdxjs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.0.tgz", - "integrity": "sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^1.0.0", - "micromark-extension-mdx-jsx": "^1.0.0", - "micromark-extension-mdx-md": "^1.0.0", - "micromark-extension-mdxjs-esm": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.3.tgz", - "integrity": "sha512-2N13ol4KMoxb85rdDwTAC6uzs8lMX0zeqpcyx7FhS7PxXomOnLactu8WI8iBNXW8AVyea3KIJd/1CKnUmwrK9A==", + "node_modules/mdast-util-from-markdown": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz", + "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==", "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.1.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-factory-destination": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", - "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mdast-util-gfm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.1.tgz", + "integrity": "sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ==", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-factory-label": { + "node_modules/mdast-util-gfm-autolink-literal": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", - "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.2.tgz", + "integrity": "sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-factory-mdx-expression": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.6.tgz", - "integrity": "sha512-WRQIc78FV7KrCfjsEf/sETopbYjElh3xAmNpLkd1ODPqxEngP42eVRGbiPEQWpRV27LzqW+XVTvQAMIIRLPnNA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.1.tgz", + "integrity": "sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw==", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-factory-space": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", - "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.1.tgz", + "integrity": "sha512-zKJbEPe+JP6EUv0mZ0tQUyLQOC+FADt0bARldONot/nefuISkaZFlmVK4tU6JgfyZGrky02m/I6PmehgAgZgqg==", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-factory-title": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", - "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "node_modules/mdast-util-gfm-table": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.4.tgz", + "integrity": "sha512-aEuoPwZyP4iIMkf2cLWXxx3EQ6Bmh2yKy9MVCg4i6Sd3cX80dcLEfXO/V4ul3pGH9czBK4kp+FAl+ZHmSUt9/w==", + "dependencies": { + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.1.tgz", + "integrity": "sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.0.tgz", + "integrity": "sha512-M09lW0CcBT1VrJUaF/PYxemxxHa7SLDHdSn94Q9FhxjCQfuW7nMAWKWimTmA3OyDMSTH981NN1csW1X+HPSluw==", + "dependencies": { + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdx-jsx": "^2.0.0", + "mdast-util-mdxjs-esm": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.0.tgz", + "integrity": "sha512-9kTO13HaL/ChfzVCIEfDRdp1m5hsvsm6+R8yr67mH+KS2ikzZ0ISGLPTbTswOFpLLlgVHO9id3cul4ajutCvCA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.0.tgz", + "integrity": "sha512-KzgzfWMhdteDkrY4mQtyvTU5bc/W4ppxhe9SzelO6QUUiwLAM+Et2Dnjjprik74a336kHdo0zKm7Tp+n6FFeRg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-to-markdown": "^1.3.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^4.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.0.tgz", + "integrity": "sha512-7N5ihsOkAEGjFotIX9p/YPdl4TqUoMxL4ajNz7PbT89BqsdWJuBC9rvgt6wpbwTZqWWR0jKWqQbwsOWDBUZv4g==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-phrasing/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/mdast-util-phrasing/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.1.2.tgz", + "integrity": "sha512-Wn6Mcj04qU4qUXHnHpPATYMH2Jd8RlntdnloDfYLe1ErWRHo6+pvSl/DzHp6sCZ9cBSYlc8Sk8pbwb8xtUoQhQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/mdurl": "^1.0.0", + "mdast-util-definitions": "^5.0.0", + "mdurl": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "trim-lines": "^3.0.0", + "unist-builder": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz", + "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", + "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "node_modules/mdx-annotations": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/mdx-annotations/-/mdx-annotations-0.1.3.tgz", + "integrity": "sha512-2XrOlQeBDUa8GirNHy/Y7BR1h/P+vzk+1G2rzAfqJ+lg6JcEOKMCqsVkpVFSw0hdzpVuj9BnoNeA+aU1XPTkoA==", + "dependencies": { + "acorn": "^8.8.1", + "estree-util-visit": "^1.2.0", + "unist-util-visit": "^4.1.1" + } + }, + "node_modules/meilisearch": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.33.0.tgz", + "integrity": "sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==", + "dependencies": { + "cross-fetch": "^3.1.6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz", + "integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==", "funding": [ { "type": "GitHub Sponsors", @@ -5980,17 +6490,29 @@ } ], "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", + "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, - "node_modules/micromark-factory-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", - "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "node_modules/micromark-core-commonmark": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", + "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", "funding": [ { "type": "GitHub Sponsors", @@ -6002,35 +6524,143 @@ } ], "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" } }, - "node_modules/micromark-util-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", - "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/micromark-extension-gfm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz", + "integrity": "sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.0.4.tgz", + "integrity": "sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg==", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.4.tgz", + "integrity": "sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ==", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz", + "integrity": "sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==", "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.1.tgz", + "integrity": "sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA==", + "dependencies": { "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-util-chunked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", - "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.3.tgz", + "integrity": "sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q==", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.3.tgz", + "integrity": "sha512-TjYtjEMszWze51NJCZmhv7MEBcgYRgb3tJeMAJ+HQCAaZHHRBaDCccqQzGizR/H4ODefP44wRTgOn2vE5I6nZA==", "funding": [ { "type": "GitHub Sponsors", @@ -6042,13 +6672,89 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/micromark-util-classify-character": { + "node_modules/micromark-extension-mdx-jsx": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.3.tgz", + "integrity": "sha512-VfA369RdqUISF0qGgv2FfV7gGjHDfn9+Qfiv5hEwpyr1xscRj/CiVRkU7rywGFCO7JwJ5L0e7CJz60lY52+qOA==", + "dependencies": { + "@types/acorn": "^4.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", - "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.0.tgz", + "integrity": "sha512-xaRAMoSkKdqZXDAoSgp20Azm0aRQKGOl0RrS81yGu8Hr/JhMsBmfs4wR7m9kgVUIO36cMUQjNyiyDKPrsv8gOw==", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.0.tgz", + "integrity": "sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^1.0.0", + "micromark-extension-mdx-jsx": "^1.0.0", + "micromark-extension-mdx-md": "^1.0.0", + "micromark-extension-mdxjs-esm": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.3.tgz", + "integrity": "sha512-2N13ol4KMoxb85rdDwTAC6uzs8lMX0zeqpcyx7FhS7PxXomOnLactu8WI8iBNXW8AVyea3KIJd/1CKnUmwrK9A==", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.1.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", + "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", "funding": [ { "type": "GitHub Sponsors", @@ -6065,10 +6771,10 @@ "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", - "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "node_modules/micromark-factory-label": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", + "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", "funding": [ { "type": "GitHub Sponsors", @@ -6080,14 +6786,16 @@ } ], "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", - "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "node_modules/micromark-factory-mdx-expression": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.6.tgz", + "integrity": "sha512-WRQIc78FV7KrCfjsEf/sETopbYjElh3xAmNpLkd1ODPqxEngP42eVRGbiPEQWpRV27LzqW+XVTvQAMIIRLPnNA==", "funding": [ { "type": "GitHub Sponsors", @@ -6099,13 +6807,20 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" } }, - "node_modules/micromark-util-decode-string": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", - "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "node_modules/micromark-factory-space": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", + "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", "funding": [ { "type": "GitHub Sponsors", @@ -6117,16 +6832,14 @@ } ], "dependencies": { - "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-util-encode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", - "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "node_modules/micromark-factory-title": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", + "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", "funding": [ { "type": "GitHub Sponsors", @@ -6136,9 +6849,167 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ] - }, - "node_modules/micromark-util-events-to-acorn": { + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", + "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", + "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", + "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", + "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", + "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", + "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", + "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", + "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-events-to-acorn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.0.tgz", "integrity": "sha512-WWp3bf7xT9MppNuw3yPjpnOxa8cj5ACivEzXJKu0WwnjBYfzaBvIAT9KfeyI0Qkll+bfQtfftSwdgTH6QhTOKw==", @@ -6293,1562 +7164,3603 @@ "picomatch": "^2.3.1" }, "engines": { - "node": ">=8.6" + "node": ">=8.6" + } + }, + "node_modules/middleearth-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/middleearth-names/-/middleearth-names-1.1.0.tgz", + "integrity": "sha512-Oo1mbq9odpn6KHsDs8/UA5xFfX/gcrY+jWZpvd5MDaX0tjkxA7S7NTREQuqD7DWfluDgygjhKvETMWbwd3A9sA==", + "dependencies": { + "unique-random-array": "1.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrm": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/mrm/-/mrm-3.0.10.tgz", + "integrity": "sha512-aRByZsPXMM8W0NHNH9afkKyk5OW4bB5pYNRIN+8iSVfpMAzqeMejmj/yIYcdFNJTksXmdPMfTaucm2NYdh4xIw==", + "dependencies": { + "git-username": "^1.0.0", + "glob": "^7.1.6", + "inquirer": "^7.0.4", + "is-directory": "^0.3.1", + "kleur": "^3.0.3", + "libnpx": "^10.2.4", + "listify": "^1.0.0", + "lodash": "^4.17.15", + "longest": "^2.0.1", + "middleearth-names": "^1.1.0", + "minimist": "^1.2.0", + "mrm-core": "^6.1.7", + "semver-utils": "^1.1.4", + "update-notifier": "^4.1.0", + "user-home": "^2.0.0", + "user-meta": "^1.0.0", + "which": "^2.0.2" + }, + "bin": { + "mrm": "bin/mrm.js" + }, + "engines": { + "node": ">=10.13" + } + }, + "node_modules/mrm-core": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/mrm-core/-/mrm-core-6.1.7.tgz", + "integrity": "sha512-jLGWrkupcgGIsLerrI/xmM/dFHbaoehRsuVbgYBrxYKXNMRBHN3Mgkd8cw+/ZCCoiZEXF8/SaZol0GCp6oBQ9g==", + "dependencies": { + "babel-code-frame": "^6.26.0", + "comment-json": "^2.2.0", + "detect-indent": "^6.0.0", + "editorconfig": "^0.15.3", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0", + "kleur": "^3.0.3", + "listify": "^1.0.0", + "lodash": "^4.17.15", + "minimist": "^1.2.0", + "prop-ini": "^0.0.2", + "rc": "^1.2.8", + "readme-badger": "^0.3.0", + "semver": "^6.3.0", + "smpltmpl": "^1.0.2", + "split-lines": "^2.0.0", + "strip-bom": "^4.0.0", + "validate-npm-package-name": "^3.0.0", + "webpack-merge": "^4.2.2", + "yaml": "^2.0.0-1" + }, + "engines": { + "node": ">=10.13" + } + }, + "node_modules/mrm-core/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mrm-core/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mrm-core/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mrm-core/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mrm-core/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mrm-core/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mrm-core/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/mrm-core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/mrm-core/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/mrm-core/node_modules/yaml": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", + "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/mrm/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "peer": true + }, + "node_modules/next": { + "version": "13.4.2", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.2.tgz", + "integrity": "sha512-aNFqLs3a3nTGvLWlO9SUhCuMUHVPSFQC0+tDNGAsDXqx+WJDFSbvc233gOJ5H19SBc7nw36A9LwQepOJ2u/8Kg==", + "dependencies": { + "@next/env": "13.4.2", + "@swc/helpers": "0.5.1", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.14", + "styled-jsx": "5.1.1", + "zod": "3.21.4" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=16.8.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "13.4.2", + "@next/swc-darwin-x64": "13.4.2", + "@next/swc-linux-arm64-gnu": "13.4.2", + "@next/swc-linux-arm64-musl": "13.4.2", + "@next/swc-linux-x64-gnu": "13.4.2", + "@next/swc-linux-x64-musl": "13.4.2", + "@next/swc-win32-arm64-msvc": "13.4.2", + "@next/swc-win32-ia32-msvc": "13.4.2", + "@next/swc-win32-x64-msvc": "13.4.2" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "fibers": ">= 3.1.0", + "node-sass": "^6.0.0 || ^7.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-mdx-remote": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-6.0.0.tgz", + "integrity": "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@mdx-js/mdx": "^3.0.1", + "@mdx-js/react": "^3.0.1", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.1", + "vfile-matter": "^5.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/next-mdx-remote/node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/next-mdx-remote/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/next-mdx-remote/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/next-mdx-remote/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/next-mdx-remote/node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/next-mdx-remote/node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/next-mdx-remote/node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-mdx-remote/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/next-router-mock": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/next-router-mock/-/next-router-mock-0.9.3.tgz", + "integrity": "sha512-jl8eFe71LpMVGeBMpoxILkGfEgGY7IfLy8XPyv05/o61p5oQRNpoMmk46VMxRIpt0fI8XcvznBZKpDK6vbYQcQ==", + "peerDependencies": { + "next": ">=10.0.0", + "react": ">=17.0.0" + } + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "dependencies": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.0.tgz", + "integrity": "sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-git-config": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-1.1.1.tgz", + "integrity": "sha512-S3LGXJZVSy/hswvbSkfdbKBRVsnqKrVu6j8fcvdtJ4TxosSELyQDsJPuGPXuZ+EyuYuJd3O4uAF8gcISR0OFrQ==", + "dependencies": { + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "git-config-path": "^1.0.1", + "ini": "^1.3.4" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/middleearth-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/middleearth-names/-/middleearth-names-1.1.0.tgz", - "integrity": "sha512-Oo1mbq9odpn6KHsDs8/UA5xFfX/gcrY+jWZpvd5MDaX0tjkxA7S7NTREQuqD7DWfluDgygjhKvETMWbwd3A9sA==", - "dependencies": { - "unique-random-array": "1.0.0" + "node_modules/parse-github-url": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", + "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", + "bin": { + "parse-github-url": "cli.js" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "engines": { - "node": ">= 0.6" + "node": ">=4" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/periscopic": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.0.4.tgz", + "integrity": "sha512-SFx68DxCv0Iyo6APZuw/AKewkkThGwssmU0QWtTlvov3VAtPX+QJ4CadwSaz8nrT5jPIuxdvJWB4PnD2KNDxQg==", "dependencies": { - "brace-expansion": "^1.1.7" - }, + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { - "node": "*" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", "engines": { - "node": ">=4" + "node": ">= 6" } }, - "node_modules/mrm": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/mrm/-/mrm-3.0.10.tgz", - "integrity": "sha512-aRByZsPXMM8W0NHNH9afkKyk5OW4bB5pYNRIN+8iSVfpMAzqeMejmj/yIYcdFNJTksXmdPMfTaucm2NYdh4xIw==", + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], "dependencies": { - "git-username": "^1.0.0", - "glob": "^7.1.6", - "inquirer": "^7.0.4", - "is-directory": "^0.3.1", - "kleur": "^3.0.3", - "libnpx": "^10.2.4", - "listify": "^1.0.0", - "lodash": "^4.17.15", - "longest": "^2.0.1", - "middleearth-names": "^1.1.0", - "minimist": "^1.2.0", - "mrm-core": "^6.1.7", - "semver-utils": "^1.1.4", - "update-notifier": "^4.1.0", - "user-home": "^2.0.0", - "user-meta": "^1.0.0", - "which": "^2.0.2" - }, - "bin": { - "mrm": "bin/mrm.js" + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" }, "engines": { - "node": ">=10.13" + "node": "^10 || ^12 || >=14" } }, - "node_modules/mrm-core": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/mrm-core/-/mrm-core-6.1.7.tgz", - "integrity": "sha512-jLGWrkupcgGIsLerrI/xmM/dFHbaoehRsuVbgYBrxYKXNMRBHN3Mgkd8cw+/ZCCoiZEXF8/SaZol0GCp6oBQ9g==", + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", "dependencies": { - "babel-code-frame": "^6.26.0", - "comment-json": "^2.2.0", - "detect-indent": "^6.0.0", - "editorconfig": "^0.15.3", - "find-up": "^4.1.0", - "fs-extra": "^8.1.0", - "kleur": "^3.0.3", - "listify": "^1.0.0", - "lodash": "^4.17.15", - "minimist": "^1.2.0", - "prop-ini": "^0.0.2", - "rc": "^1.2.8", - "readme-badger": "^0.3.0", - "semver": "^6.3.0", - "smpltmpl": "^1.0.2", - "split-lines": "^2.0.0", - "strip-bom": "^4.0.0", - "validate-npm-package-name": "^3.0.0", - "webpack-merge": "^4.2.2", - "yaml": "^2.0.0-1" + "postcss-selector-parser": "^6.0.9" }, "engines": { - "node": ">=10.13" + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/mrm-core/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" }, "engines": { - "node": ">=8" + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" } }, - "node_modules/mrm-core/node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" } }, - "node_modules/mrm-core/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", "dependencies": { - "p-locate": "^4.1.0" + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/mrm-core/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", "dependencies": { - "p-try": "^2.0.0" + "postcss-selector-parser": "^6.0.10" }, "engines": { - "node": ">=6" + "node": ">=12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" } }, - "node_modules/mrm-core/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dependencies": { - "p-limit": "^2.2.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/mrm-core/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/mrm-core/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/mrm-core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true, "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/mrm-core/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "prettier": "bin-prettier.js" + }, "engines": { - "node": ">=8" + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/mrm-core/node_modules/yaml": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", - "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.6.tgz", + "integrity": "sha512-F+7XCl9RLF/LPrGdUMHWpsT6TM31JraonAUyE6eBmpqymFvDwyl0ETHsKFHP1NG+sEfv8bmKqnTxEbWQbHPlBA==", + "dev": true, "engines": { - "node": ">= 14" + "node": ">=12.17.0" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-php": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@shufo/prettier-plugin-blade": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "prettier": ">=2.2.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*", + "prettier-plugin-twig-melody": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-php": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@shufo/prettier-plugin-blade": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "prettier-plugin-twig-melody": { + "optional": true + } } }, - "node_modules/mrm/node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/prism-themes": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/prism-themes/-/prism-themes-1.9.0.tgz", + "integrity": "sha512-tX2AYsehKDw1EORwBps+WhBFKc2kxfoFpQAjxBndbZKr4fRmMkv47XN0BghC/K1qwodB1otbe4oF23vUTFDokw==" + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", "engines": { "node": ">=6" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + "node_modules/prop-ini": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/prop-ini/-/prop-ini-0.0.2.tgz", + "integrity": "sha512-qyU57WvAvZDbzmRy9xDbJGVwrGJhmA+rYnVjy4xtX4Ny9c7gzvpmf/j7A3oq9ChbPh15MZQKjPep2mNdnAhtig==", + "dependencies": { + "extend": "^3.0.0" + } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node_modules/property-information": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", + "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "peer": true + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, - "node_modules/next": { - "version": "13.4.2", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.2.tgz", - "integrity": "sha512-aNFqLs3a3nTGvLWlO9SUhCuMUHVPSFQC0+tDNGAsDXqx+WJDFSbvc233gOJ5H19SBc7nw36A9LwQepOJ2u/8Kg==", + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", "dependencies": { - "@next/env": "13.4.2", - "@swc/helpers": "0.5.1", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.14", - "styled-jsx": "5.1.1", - "zod": "3.21.4" - }, - "bin": { - "next": "dist/bin/next" + "escape-goat": "^2.0.0" }, "engines": { - "node": ">=16.8.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "13.4.2", - "@next/swc-darwin-x64": "13.4.2", - "@next/swc-linux-arm64-gnu": "13.4.2", - "@next/swc-linux-arm64-musl": "13.4.2", - "@next/swc-linux-x64-gnu": "13.4.2", - "@next/swc-linux-x64-musl": "13.4.2", - "@next/swc-win32-arm64-msvc": "13.4.2", - "@next/swc-win32-ia32-msvc": "13.4.2", - "@next/swc-win32-x64-msvc": "13.4.2" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "fibers": ">= 3.1.0", - "node-sass": "^6.0.0 || ^7.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "fibers": { - "optional": true + "node": ">=8" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "node-sass": { - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "sass": { - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ] }, - "node_modules/next-mdx-remote": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-4.4.1.tgz", - "integrity": "sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==", - "dependencies": { - "@mdx-js/mdx": "^2.2.1", - "@mdx-js/react": "^2.2.1", - "vfile": "^5.3.0", - "vfile-matter": "^3.0.1" - }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "engines": { - "node": ">=14", - "npm": ">=7" + "node": ">=10" }, - "peerDependencies": { - "react": ">=16.x <=18.x", - "react-dom": ">=16.x <=18.x" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/next-router-mock": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/next-router-mock/-/next-router-mock-0.9.3.tgz", - "integrity": "sha512-jl8eFe71LpMVGeBMpoxILkGfEgGY7IfLy8XPyv05/o61p5oQRNpoMmk46VMxRIpt0fI8XcvznBZKpDK6vbYQcQ==", - "peerDependencies": { - "next": ">=10.0.0", - "react": ">=17.0.0" + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" } }, - "node_modules/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "bin": { + "rc": "cli.js" } }, - "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "engines": { "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm-package-arg": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", - "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { - "hosted-git-info": "^2.7.1", - "osenv": "^0.1.5", - "semver": "^5.6.0", - "validate-npm-package-name": "^3.0.0" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" } }, - "node_modules/npm-package-arg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", "dependencies": { - "path-key": "^2.0.0" + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "engines": { - "node": ">=4" + "node_modules/react-markdown/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-markdown/node_modules/style-to-object": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", + "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", + "dependencies": { + "inline-style-parser": "0.1.1" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "pify": "^2.3.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" + "node_modules/readme-badger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/readme-badger/-/readme-badger-0.3.0.tgz", + "integrity": "sha512-+sMOLSs1imZUISZ2Rhz7qqVd77QtpcAPbGeIraFdgJmijb04YtdlPjGNBvDChTNtLbeQ6JNGQy3pOgslWfaP3g==", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "dev": true, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } + "node_modules/recma-build-jsx/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, + "node_modules/recma-build-jsx/node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" + "node_modules/recma-build-jsx/node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, + "node_modules/recma-build-jsx/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" + "@types/unist": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/object.hasown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", - "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", - "dev": true, + "node_modules/recma-build-jsx/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "dev": true, + "node_modules/recma-build-jsx/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/recma-import-images": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/recma-import-images/-/recma-import-images-0.0.3.tgz", + "integrity": "sha512-XoPDnUP8XVH13UrSfvdawq3gcZYoOVRoW2CtYHMR68hRejCAhLmgjjgPY/Xf1Ftkjj5ASD8wx9eVNjYTW2yWbw==", "dependencies": { - "wrappy": "1" + "@sindresorhus/slugify": "^2.2.0", + "estree-util-visit": "^1.2.1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type": "opencollective", + "url": "https://opencollective.com/unified" }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } + "node_modules/recma-jsx/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "node_modules/recma-jsx/node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "engines": { - "node": ">=6" + "node_modules/recma-jsx/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "engines": { - "node": ">=4" + "node_modules/recma-jsx/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, + "node_modules/recma-jsx/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dependencies": { - "p-try": "^1.0.0" + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, + "node_modules/recma-jsx/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dependencies": { - "p-limit": "^1.1.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/p-try": { + "node_modules/recma-nextjs-static-props": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true, + "resolved": "https://registry.npmjs.org/recma-nextjs-static-props/-/recma-nextjs-static-props-1.0.0.tgz", + "integrity": "sha512-szo+rOZFU6mR0YWZi3e3dSqcEQU+E0f7GIyfMfntHeJccH1s9ODP0HWUeK7No0lcY1smRCcC43JrpoekzuX4Aw==", + "dependencies": { + "@types/estree": "*", + "periscopic": "^3.0.0", + "unified": "^10.0.0" + }, "engines": { - "node": ">=4" + "node": ">=14.0.0" } }, - "node_modules/package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", "dependencies": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/package-json/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } + "node_modules/recma-parse/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, + "node_modules/recma-parse/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "dependencies": { - "callsites": "^3.0.0" + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/parse-entities": { + "node_modules/recma-parse/node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.0.tgz", - "integrity": "sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dependencies": { - "@types/unist": "^2.0.0", - "character-entities": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/parse-git-config": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-1.1.1.tgz", - "integrity": "sha512-S3LGXJZVSy/hswvbSkfdbKBRVsnqKrVu6j8fcvdtJ4TxosSELyQDsJPuGPXuZ+EyuYuJd3O4uAF8gcISR0OFrQ==", + "node_modules/recma-parse/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dependencies": { - "extend-shallow": "^2.0.1", - "fs-exists-sync": "^0.1.0", - "git-config-path": "^1.0.1", - "ini": "^1.3.4" + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/parse-github-url": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", - "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", - "bin": { - "parse-github-url": "cli.js" + "node_modules/recma-parse/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/parse-passwd": { + "node_modules/recma-stringify": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "engines": { - "node": ">=0.10.0" + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "engines": { - "node": ">=4" - } + "node_modules/recma-stringify/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" + "node_modules/recma-stringify/node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" + "node_modules/recma-stringify/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-type": { + "node_modules/recma-stringify/node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/periscopic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.0.4.tgz", - "integrity": "sha512-SFx68DxCv0Iyo6APZuw/AKewkkThGwssmU0QWtTlvov3VAtPX+QJ4CadwSaz8nrT5jPIuxdvJWB4PnD2KNDxQg==", + "node_modules/recma-stringify/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dependencies": { - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "node_modules/recma-stringify/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "engines": { - "node": ">= 6" + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss-focus-visible": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", - "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=10.0.0" + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" }, - "peerDependencies": { - "postcss": "^8.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": ">=12.0" + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, "engines": { - "node": ">=10.13.0" + "node": ">=8" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/prettier-plugin-tailwindcss": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.6.tgz", - "integrity": "sha512-F+7XCl9RLF/LPrGdUMHWpsT6TM31JraonAUyE6eBmpqymFvDwyl0ETHsKFHP1NG+sEfv8bmKqnTxEbWQbHPlBA==", - "dev": true, - "engines": { - "node": ">=12.17.0" + "node_modules/registry-auth-token": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", + "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", + "dependencies": { + "rc": "1.2.8" }, - "peerDependencies": { - "@ianvs/prettier-plugin-sort-imports": "*", - "@prettier/plugin-php": "*", - "@prettier/plugin-pug": "*", - "@shopify/prettier-plugin-liquid": "*", - "@shufo/prettier-plugin-blade": "*", - "@trivago/prettier-plugin-sort-imports": "*", - "prettier": ">=2.2.0", - "prettier-plugin-astro": "*", - "prettier-plugin-css-order": "*", - "prettier-plugin-import-sort": "*", - "prettier-plugin-jsdoc": "*", - "prettier-plugin-organize-attributes": "*", - "prettier-plugin-organize-imports": "*", - "prettier-plugin-style-order": "*", - "prettier-plugin-svelte": "*", - "prettier-plugin-twig-melody": "*" + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dependencies": { + "rc": "^1.2.8" }, - "peerDependenciesMeta": { - "@ianvs/prettier-plugin-sort-imports": { - "optional": true - }, - "@prettier/plugin-php": { - "optional": true - }, - "@prettier/plugin-pug": { - "optional": true - }, - "@shopify/prettier-plugin-liquid": { - "optional": true - }, - "@shufo/prettier-plugin-blade": { - "optional": true - }, - "@trivago/prettier-plugin-sort-imports": { - "optional": true - }, - "prettier-plugin-astro": { - "optional": true - }, - "prettier-plugin-css-order": { - "optional": true - }, - "prettier-plugin-import-sort": { - "optional": true - }, - "prettier-plugin-jsdoc": { - "optional": true - }, - "prettier-plugin-organize-attributes": { - "optional": true - }, - "prettier-plugin-organize-imports": { - "optional": true - }, - "prettier-plugin-style-order": { - "optional": true - }, - "prettier-plugin-svelte": { - "optional": true - }, - "prettier-plugin-twig-melody": { - "optional": true - } + "engines": { + "node": ">=8" + } + }, + "node_modules/rehype-autolink-headings": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz", + "integrity": "sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==", + "dependencies": { + "@types/hast": "^2.0.0", + "extend": "^3.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/prism-themes": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/prism-themes/-/prism-themes-1.9.0.tgz", - "integrity": "sha512-tX2AYsehKDw1EORwBps+WhBFKc2kxfoFpQAjxBndbZKr4fRmMkv47XN0BghC/K1qwodB1otbe4oF23vUTFDokw==" - }, - "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "node_modules/rehype-mdx-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rehype-mdx-title/-/rehype-mdx-title-2.0.0.tgz", + "integrity": "sha512-IemxnNjM+mrABwH2V0UQjg5YULJmN55dF+zEajmoDgjnuAESIIm54iSKR0VwKpFrvQ9hWLn88RTr2deqwSOw0A==", + "dependencies": { + "@types/hast": "^2.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, "engines": { - "node": ">=6" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" } }, - "node_modules/prop-ini": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/prop-ini/-/prop-ini-0.0.2.tgz", - "integrity": "sha512-qyU57WvAvZDbzmRy9xDbJGVwrGJhmA+rYnVjy4xtX4Ny9c7gzvpmf/j7A3oq9ChbPh15MZQKjPep2mNdnAhtig==", + "node_modules/rehype-mdx-title/node_modules/hast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", + "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", "dependencies": { - "extend": "^3.0.0" + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/property-information": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", - "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==", + "node_modules/rehype-recma/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/rehype-recma/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/rehype-recma/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/rehype-recma/node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "dependencies": { + "@types/estree": "^1.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "node_modules/rehype-recma/node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + "node_modules/rehype-recma/node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/pump": { + "node_modules/rehype-recma/node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" + "node_modules/rehype-recma/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "node_modules/rehype-recma/node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "dependencies": { - "escape-goat": "^2.0.0" + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/rehype-recma/node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" + "node_modules/rehype-recma/node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "peer": true, + "node_modules/rehype-recma/node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "dependencies": { - "safe-buffer": "^5.1.0" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "node_modules/rehype-recma/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "@types/mdast": "^4.0.0" }, - "bin": { - "rc": "cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/rehype-recma/node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/rehype-recma/node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/rc/node_modules/strip-json-comments": { + "node_modules/rehype-recma/node_modules/micromark-factory-label": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "node_modules/rehype-recma/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/react-markdown": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", - "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "node_modules/rehype-recma/node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@types/hast": "^2.0.0", - "@types/prop-types": "^15.0.0", - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "prop-types": "^15.0.0", - "property-information": "^6.0.0", - "react-is": "^18.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^10.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/react-markdown/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" - }, - "node_modules/react-markdown/node_modules/style-to-object": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", - "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", + "node_modules/rehype-recma/node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "inline-style-parser": "0.1.1" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "node_modules/rehype-recma/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "pify": "^2.3.0" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/rehype-recma/node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/readme-badger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/readme-badger/-/readme-badger-0.3.0.tgz", - "integrity": "sha512-+sMOLSs1imZUISZ2Rhz7qqVd77QtpcAPbGeIraFdgJmijb04YtdlPjGNBvDChTNtLbeQ6JNGQy3pOgslWfaP3g==", + "node_modules/rehype-recma/node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "balanced-match": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/recma-import-images": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/recma-import-images/-/recma-import-images-0.0.3.tgz", - "integrity": "sha512-XoPDnUP8XVH13UrSfvdawq3gcZYoOVRoW2CtYHMR68hRejCAhLmgjjgPY/Xf1Ftkjj5ASD8wx9eVNjYTW2yWbw==", + "node_modules/rehype-recma/node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@sindresorhus/slugify": "^2.2.0", - "estree-util-visit": "^1.2.1" + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/recma-nextjs-static-props": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-nextjs-static-props/-/recma-nextjs-static-props-1.0.0.tgz", - "integrity": "sha512-szo+rOZFU6mR0YWZi3e3dSqcEQU+E0f7GIyfMfntHeJccH1s9ODP0HWUeK7No0lcY1smRCcC43JrpoekzuX4Aw==", + "node_modules/rehype-recma/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@types/estree": "*", - "periscopic": "^3.0.0", - "unified": "^10.0.0" - }, - "engines": { - "node": ">=14.0.0" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "node_modules/rehype-recma/node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/refractor/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/rehype-recma/node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, - "node_modules/refractor/node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/rehype-recma/node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, - "node_modules/refractor/node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/rehype-recma/node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/refractor/node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/rehype-recma/node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" } }, - "node_modules/refractor/node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "node_modules/rehype-recma/node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/refractor/node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/rehype-recma/node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/refractor/node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/rehype-recma/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, - "node_modules/refractor/node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - }, + "node_modules/rehype-recma/node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/rehype-recma/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, + "node_modules/rehype-recma/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" + "@types/unist": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" + "node_modules/rehype-recma/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/registry-auth-token": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", - "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", + "node_modules/rehype-recma/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dependencies": { - "rc": "1.2.8" + "@types/unist": "^3.0.0" }, - "engines": { - "node": ">=6.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/registry-url": { + "node_modules/rehype-recma/node_modules/unist-util-visit": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dependencies": { - "rc": "^1.2.8" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/rehype-autolink-headings": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz", - "integrity": "sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "dependencies": { - "@types/hast": "^2.0.0", - "extend": "^3.0.0", - "hast-util-has-property": "^2.0.0", - "hast-util-heading-rank": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/rehype-mdx-title": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/rehype-mdx-title/-/rehype-mdx-title-2.0.0.tgz", - "integrity": "sha512-IemxnNjM+mrABwH2V0UQjg5YULJmN55dF+zEajmoDgjnuAESIIm54iSKR0VwKpFrvQ9hWLn88RTr2deqwSOw0A==", + "node_modules/rehype-recma/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dependencies": { - "@types/hast": "^2.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "hast-util-to-string": "^2.0.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0" - }, - "engines": { - "node": ">=14" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/remcohaszing" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/rehype-mdx-title/node_modules/hast-util-to-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", - "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "node_modules/rehype-recma/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dependencies": { - "@types/hast": "^2.0.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", @@ -8503,6 +11415,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-js/node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==" + }, + "node_modules/style-to-js/node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -9016,32 +11949,76 @@ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.3.tgz", "integrity": "sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.1.tgz", + "integrity": "sha512-xtoY50b5+7IH8tFbkw64gisG9tMSpxDjhX9TmaJJae/XuxQ9R/Kc8Nv1eOsf43Gt4KV/LkriMy9mptDr7XLcaw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.1.tgz", + "integrity": "sha512-0yDkppiIhDlPrfHELgB+NLQD5mfjup3a8UYclHruTJWmY74je8g+CIFr79x5f6AkmzSwlvKLbs63hC0meOMowQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-position-from-estree": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.1.tgz", - "integrity": "sha512-xtoY50b5+7IH8tFbkw64gisG9tMSpxDjhX9TmaJJae/XuxQ9R/Kc8Nv1eOsf43Gt4KV/LkriMy9mptDr7XLcaw==", + "node_modules/unist-util-remove/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/unist-util-remove/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-remove-position": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.1.tgz", - "integrity": "sha512-0yDkppiIhDlPrfHELgB+NLQD5mfjup3a8UYclHruTJWmY74je8g+CIFr79x5f6AkmzSwlvKLbs63hC0meOMowQ==", + "node_modules/unist-util-remove/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", @@ -9289,19 +12266,75 @@ } }, "node_modules/vfile-matter": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-3.0.1.tgz", - "integrity": "sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-5.0.1.tgz", + "integrity": "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==", "dependencies": { - "@types/js-yaml": "^4.0.0", - "is-buffer": "^2.0.0", - "js-yaml": "^4.0.0" + "vfile": "^6.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/vfile-matter/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-matter/node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/vfile-message": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz", @@ -9965,6 +12998,21 @@ "@algolia/requester-common": "4.17.2" } }, + "@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "requires": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==" + }, "@babel/runtime": { "version": "7.17.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", @@ -10412,11 +13460,6 @@ "@types/unist": "*" } }, - "@types/js-yaml": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", - "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==" - }, "@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -10539,6 +13582,11 @@ "eslint-visitor-keys": "^3.0.0" } }, + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, "@vercel/analytics": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.2.tgz", @@ -10822,7 +13870,8 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "aria-query": { "version": "4.2.2", @@ -11304,6 +14353,11 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" }, + "collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -11498,6 +14552,14 @@ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==" }, + "devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "requires": { + "dequal": "^2.0.0" + } + }, "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -11668,6 +14730,76 @@ "is-symbol": "^1.0.2" } }, + "esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + } + }, + "unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "requires": { + "@types/unist": "^3.0.0" + } + } + } + }, + "esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -12082,6 +15214,15 @@ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.0.1.tgz", "integrity": "sha512-rxZj1GkQhY4x1j/CSnybK9cGuMFQYFPLq0iNyopqf14aOVLFtMv7Esika+ObJWPWiOHuMOAHz3YkWoLYYRnzWQ==" }, + "estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "requires": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + } + }, "estree-util-to-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz", @@ -12676,6 +15817,420 @@ "zwitch": "^2.0.0" } }, + "hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "requires": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==" + }, + "hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "requires": { + "@types/hast": "^3.0.0" + } + }, + "mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + } + }, + "mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + } + }, + "mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + } + }, + "mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "requires": { + "@types/mdast": "^4.0.0" + } + }, + "micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "requires": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "requires": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" + }, + "micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==" + }, + "micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "requires": { + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" + }, + "micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" + }, + "property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" + }, + "unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } + } + }, "hast-util-to-string": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-1.0.4.tgz", @@ -13120,6 +16675,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "requires": { "argparse": "^2.0.1" } @@ -13861,6 +17417,38 @@ "mdast-util-to-markdown": "^1.0.0" } }, + "mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "requires": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "dependencies": { + "@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "requires": { + "@types/unist": "^3.0.0" + } + } + } + }, "mdast-util-to-hast": { "version": "12.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.1.2.tgz", @@ -14570,14 +18158,642 @@ } }, "next-mdx-remote": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-4.4.1.tgz", - "integrity": "sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-6.0.0.tgz", + "integrity": "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==", "requires": { - "@mdx-js/mdx": "^2.2.1", - "@mdx-js/react": "^2.2.1", - "vfile": "^5.3.0", - "vfile-matter": "^3.0.1" + "@babel/code-frame": "^7.23.5", + "@mdx-js/mdx": "^3.0.1", + "@mdx-js/react": "^3.0.1", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.1", + "vfile-matter": "^5.0.0" + }, + "dependencies": { + "@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "requires": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + } + }, + "@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "requires": { + "@types/mdx": "^2.0.0" + } + }, + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==" + }, + "estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + } + }, + "markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==" + }, + "mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + } + }, + "mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "requires": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + } + }, + "mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + } + }, + "mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + } + }, + "mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "requires": { + "@types/mdast": "^4.0.0" + } + }, + "micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "requires": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "requires": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "requires": { + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "requires": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "requires": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "requires": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "requires": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "requires": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" + }, + "micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "requires": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==" + }, + "micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "requires": { + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" + }, + "micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" + }, + "remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "requires": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + } + }, + "remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "requires": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + } + }, + "remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + } + }, + "unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "requires": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + } + }, + "unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + } + }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } } }, "next-router-mock": { @@ -14920,9 +19136,9 @@ } }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "picomatch": { "version": "2.3.1", @@ -15215,6 +19431,65 @@ "balanced-match": "^1.0.0" } }, + "recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "requires": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + } + }, + "estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==" + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } + } + }, "recma-import-images": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/recma-import-images/-/recma-import-images-0.0.3.tgz", @@ -15224,6 +19499,75 @@ "estree-util-visit": "^1.2.1" } }, + "recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "requires": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + } + }, + "unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "requires": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } + } + }, "recma-nextjs-static-props": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/recma-nextjs-static-props/-/recma-nextjs-static-props-1.0.0.tgz", @@ -15234,6 +19578,132 @@ "unified": "^10.0.0" } }, + "recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "requires": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "requires": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } + } + }, + "recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "requires": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + } + }, + "unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "requires": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } + } + }, "refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", @@ -15378,6 +19848,439 @@ } } }, + "rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "requires": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "dependencies": { + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, + "@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "requires": { + "@types/unist": "*" + } + }, + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "requires": { + "@types/estree": "^1.0.0" + } + }, + "estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==" + }, + "hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "requires": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + } + }, + "hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "requires": { + "@types/hast": "^3.0.0" + } + }, + "mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + } + }, + "mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + } + }, + "mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + } + }, + "mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "requires": { + "@types/mdast": "^4.0.0" + } + }, + "micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "requires": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "requires": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" + }, + "micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==" + }, + "micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "requires": { + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" + }, + "micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" + }, + "property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" + }, + "unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + } + } + }, "rehype-slug": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.1.0.tgz", @@ -15848,6 +20751,29 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "requires": { + "style-to-object": "1.0.14" + }, + "dependencies": { + "inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==" + }, + "style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "requires": { + "inline-style-parser": "0.2.7" + } + } + } + }, "style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -16205,6 +21131,40 @@ "@types/unist": "^2.0.0" } }, + "unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + } + } + } + }, "unist-util-remove-position": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.1.tgz", @@ -16385,13 +21345,50 @@ } }, "vfile-matter": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-3.0.1.tgz", - "integrity": "sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-5.0.1.tgz", + "integrity": "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==", "requires": { - "@types/js-yaml": "^4.0.0", - "is-buffer": "^2.0.0", - "js-yaml": "^4.0.0" + "vfile": "^6.0.0", + "yaml": "^2.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + }, + "yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==" + } } }, "vfile-message": { diff --git a/docs_src/package.json b/docs_src/package.json index d5ec0d7e4..fcbd3c3f4 100644 --- a/docs_src/package.json +++ b/docs_src/package.json @@ -33,7 +33,7 @@ "mdx-annotations": "^0.1.3", "meilisearch": "^0.33.0", "next": "13.4.2", - "next-mdx-remote": "^4.4.1", + "next-mdx-remote": "^6.0.0", "next-router-mock": "^0.9.3", "postcss-focus-visible": "^6.0.4", "prism-themes": "^1.9.0", From 18b79d9e1d48f609f8099542ab6dc63879ba3000 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:45:09 +0000 Subject: [PATCH 036/106] fix: one final json handling bug (#1313) * fix: json array parsing * update docs --- integration_tests/base_routes.py | 15 ++++++ .../helpers/http_methods_helpers.py | 4 +- integration_tests/test_json_types.py | 47 +++++++++++++++++++ robyn/robyn.pyi | 5 +- src/types/request.rs | 19 ++------ 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 31a6ece72..8e93f2106 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -753,6 +753,21 @@ async def async_json_types(request: Request): return result +# JSON top-level array test (Issue #1145) +@app.post("/sync/request_json/array") +def sync_json_array(request: Request): + """Returns the parsed JSON when the body is a top-level array""" + data = request.json() + return {"parsed": data, "type": type(data).__name__} + + +@app.post("/async/request_json/array") +async def async_json_array(request: Request): + """Returns the parsed JSON when the body is a top-level array""" + data = request.json() + return {"parsed": data, "type": type(data).__name__} + + # --- PUT --- # dict diff --git a/integration_tests/helpers/http_methods_helpers.py b/integration_tests/helpers/http_methods_helpers.py index 9ad39f2e3..75ad5eeda 100644 --- a/integration_tests/helpers/http_methods_helpers.py +++ b/integration_tests/helpers/http_methods_helpers.py @@ -63,7 +63,7 @@ def post( def json_post( endpoint: str, - json_data: Optional[dict] = None, + json_data=None, expected_status_code: int = 200, headers: dict = {}, should_check_response: bool = True, @@ -72,7 +72,7 @@ def json_post( Makes a POST request with JSON body to the given endpoint and checks the response. endpoint str: The endpoint to make the request to. - json_data Optional[dict]: The JSON data to send with the request. + json_data: The JSON-serializable data to send with the request (dict, list, etc.). expected_status_code int: The expected status code of the response. headers dict: The headers to send with the request. should_check_response bool: A boolean to indicate if the status code and headers should be checked. diff --git a/integration_tests/test_json_types.py b/integration_tests/test_json_types.py index 885f414e8..093872c04 100644 --- a/integration_tests/test_json_types.py +++ b/integration_tests/test_json_types.py @@ -119,6 +119,53 @@ def test_json_mixed_types_preserved(function_type: str, session): assert result["field_value"]["type"] == "str" +# ===== Top-level JSON Array Parsing Tests (Issue #1145) ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_top_level_array_of_strings(function_type: str, session): + """Test that request.json() handles a top-level array of strings (exact scenario from #1145)""" + json_data = ["google_docs", "notion"] + res = json_post(f"/{function_type}/request_json/array", json_data=json_data) + result = res.json() + + assert result["type"] == "list" + assert result["parsed"] == ["google_docs", "notion"] + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_top_level_array_of_objects(function_type: str, session): + """Test that request.json() handles a top-level array of objects""" + json_data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] + res = json_post(f"/{function_type}/request_json/array", json_data=json_data) + result = res.json() + + assert result["type"] == "list" + assert result["parsed"] == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_top_level_empty_array(function_type: str, session): + """Test that request.json() handles an empty top-level array""" + json_data = [] + res = json_post(f"/{function_type}/request_json/array", json_data=json_data) + result = res.json() + + assert result["type"] == "list" + assert result["parsed"] == [] + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_top_level_array_of_mixed_types(function_type: str, session): + """Test that request.json() handles a top-level array with mixed types""" + json_data = [1, "two", True, None, {"key": "value"}] + res = json_post(f"/{function_type}/request_json/array", json_data=json_data) + result = res.json() + + assert result["type"] == "list" + assert result["parsed"] == [1, "two", True, None, {"key": "value"}] + + # ===== JSON List Serialization Tests (Issue #1300) ===== diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi index d9e128aff..9a7d2a9c6 100644 --- a/robyn/robyn.pyi +++ b/robyn/robyn.pyi @@ -391,9 +391,10 @@ class Request: ip_addr: Optional[str] identity: Optional[Identity] - def json(self) -> dict: + def json(self) -> Union[dict, list]: """ - If the body is a valid JSON this will return the parsed JSON data. + If the body is valid JSON, this will return the parsed JSON data + as a dict (for JSON objects) or a list (for JSON arrays). Otherwise, this will throw a ValueError. """ pass diff --git a/src/types/request.rs b/src/types/request.rs index 6e5860d68..39d4f5544 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -276,20 +276,11 @@ impl PyRequest { pub fn json(&self, py: Python) -> PyResult> { match self.body.downcast_bound::(py) { - Ok(python_string) => match serde_json::from_str(python_string.extract()?) { - Ok(Value::Object(map)) => { - let dict = PyDict::new(py); - - for (key, value) in map.iter() { - let py_key = key.to_string().into_pyobject(py)?.into_any(); - let py_value = json_value_to_py(py, value)?; - dict.set_item(py_key, py_value)?; - } - - Ok(dict.into_pyobject(py)?.into_any().into()) - } - _ => Err(PyValueError::new_err("Invalid JSON object")), - }, + Ok(python_string) => { + let parsed: Value = serde_json::from_str(python_string.extract()?) + .map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?; + json_value_to_py(py, &parsed) + } Err(e) => Err(e.into()), } } From 9ab047806f5587cf159d9d62f5d885dde53a53a0 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:45:37 +0000 Subject: [PATCH 037/106] chore: update critical dependencies (#1310) --- pyproject.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 58c5ce3ba..4ecd02ecb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,16 +26,16 @@ classifiers = [ ] dependencies = [ "inquirerpy == 0.3.4", - "multiprocess == 0.70.14", + "multiprocess >= 0.70.18, < 0.71.0", "orjson >= 3.11.5, < 4.0.0", "rustimport == 1.3.4", # conditional "uvloop~=0.22.1; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'", - "watchdog == 4.0.1", + "watchdog >= 6.0.0, < 7.0.0", ] [project.optional-dependencies] -"templating" = ["jinja2 == 3.0.1"] +"templating" = ["jinja2 >= 3.1.6, < 4.0.0"] [project.urls] Documentation = "https://robyn.tech/" @@ -53,7 +53,7 @@ dev = [ "commitizen==2.40", "isort==5.11.5", "maturin==1.7.4", - "pre-commit==2.21.0", + "pre-commit>=4.5.1,<5.0.0", "ruff>=0.9.0", ] test = [ @@ -76,10 +76,10 @@ authors = ["Sanskar Jethi "] python = "^3.10" inquirerpy = "0.3.4" maturin = "1.7.4" -watchdog = "4.0.1" -multiprocess = "0.70.14" +watchdog = "^6.0.0" +multiprocess = "^0.70.18" uvloop = { version = "0.22.1", markers = "sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')" } -jinja2 = { version = "3.0.1", optional = true } +jinja2 = { version = "^3.1.6", optional = true } rustimport = "^1.3.4" orjson = "^3.11.5" @@ -93,7 +93,7 @@ optional = true ruff = ">=0.9.0" black = "23.1" isort = "5.11.5" -pre-commit = "2.21.0" +pre-commit = "^4.5.1" commitizen = "2.40" [tool.poetry.group.test] From a9fa2ba6b56de405dee57e781b794d68ba952e45 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:43:18 +0000 Subject: [PATCH 038/106] chore: update README with an updated list of features (#1316) --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6491a6769..230c4b2d7 100644 --- a/README.md +++ b/README.md @@ -118,20 +118,30 @@ python --version ## 💡 Features - Under active development! -- Written in Rust, btw xD - A multithreaded Runtime - Extensible -- Automatic OpenAPI generation - A simple API - Sync and Async Function Support - Dynamic URL Routing - Multi Core Scaling -- WebSockets! -- Middlewares +- WebSockets +- Middlewares (before and after request hooks) - Built in form data handling - Dependency Injection - Hot Reloading - Direct Rust Integration +- Automatic OpenAPI generation +- Jinja2 Templating +- Static File Serving +- File Responses and Downloads +- Authentication Support +- CORS Configuration +- Streaming / SSE Responses +- Startup and Shutdown Events +- Exception Handling +- SubRouters +- Project Scaffolding via CLI +- Experimental io-uring Support - **🤖 AI Agent Support** - Built-in agent routing and execution - **🔌 MCP (Model Context Protocol)** - Connect to AI applications as a server - Community First and truly FOSS! From 5abd4c400007514e71cbe6ad0ba9effd9bb6bd0d Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:29:01 +0000 Subject: [PATCH 039/106] feat: add JsonBody param for parsed JSON in callbacks and OpenAPI docs (#1095) (#1311) * feat: add JsonBody param for parsed JSON in callbacks and OpenAPI docs (#1095) * update * update * update * update * update --- .../en/api_reference/getting_started.mdx | 20 ++++---- .../en/api_reference/request_object.mdx | 29 ++++++++++- .../zh/api_reference/request_object.mdx | 29 ++++++++++- integration_tests/base_routes.py | 43 +++++++++++++++- integration_tests/test_openapi.py | 49 +++++++++++++++++++ .../test_split_request_params.py | 19 ++++++- robyn/__init__.py | 3 +- robyn/openapi.py | 6 ++- robyn/robyn.pyi | 17 +++++-- robyn/router.py | 13 ++++- robyn/types.py | 37 +++++++++++++- 11 files changed, 241 insertions(+), 24 deletions(-) diff --git a/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx b/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx index 0cf3abe78..5b9b50512 100644 --- a/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/getting_started.mdx @@ -367,7 +367,6 @@ Robyn supports all standard HTTP methods. Here's how to create a complete RESTfu ```python from robyn import Robyn, Request - import json app = Robyn(__file__) @@ -394,8 +393,8 @@ Robyn supports all standard HTTP methods. Here's how to create a complete RESTfu # POST - Create new post @app.post("/posts") - def create_post(body): - data = json.loads(body) + def create_post(request: Request): + data = request.json() post_id = str(len(posts) + 1) new_post = { "id": post_id, @@ -407,12 +406,12 @@ Robyn supports all standard HTTP methods. Here's how to create a complete RESTfu # PUT - Update entire post @app.put("/posts/:id") - def update_post(path_params, body): + def update_post(request: Request, path_params): post_id = path_params["id"] if post_id not in posts: return {"error": "Post not found"}, 404 - data = json.loads(body) + data = request.json() posts[post_id] = { "id": post_id, "title": data.get("title", ""), @@ -422,12 +421,12 @@ Robyn supports all standard HTTP methods. Here's how to create a complete RESTfu # PATCH - Partial update @app.patch("/posts/:id") - def patch_post(path_params, body): + def patch_post(request: Request, path_params): post_id = path_params["id"] if post_id not in posts: return {"error": "Post not found"}, 404 - data = json.loads(body) + data = request.json() post = posts[post_id] # Update only provided fields @@ -470,7 +469,6 @@ Robyn automatically handles JSON serialization, but also provides flexible respo ```python from robyn import Robyn, Request from datetime import datetime - import json app = Robyn(__file__) @@ -516,9 +514,9 @@ Robyn automatically handles JSON serialization, but also provides flexible respo # Custom status codes with JSON @app.post("/api/posts") - def create_post(body): + def create_post(request: Request): try: - data = json.loads(body) + data = request.json() # Validate required fields if not data.get("title"): return {"error": "Title is required"}, 400 @@ -532,7 +530,7 @@ Robyn automatically handles JSON serialization, but also provides flexible respo "created_at": datetime.now().isoformat() } }, 201 - except json.JSONDecodeError: + except ValueError: return {"error": "Invalid JSON format"}, 400 ``` diff --git a/docs_src/src/pages/documentation/en/api_reference/request_object.mdx b/docs_src/src/pages/documentation/en/api_reference/request_object.mdx index be01c32cc..d5f5fce9c 100644 --- a/docs_src/src/pages/documentation/en/api_reference/request_object.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/request_object.mdx @@ -26,7 +26,7 @@ headers (dict[str, str]): The headers of the request. `e.g. {"Content-Type": "ap params (dict[str, str]): The parameters of the request. `e.g. /user/:id -> {"id": "123"}`
  • -body (Union[str, bytes]): The body of the request. If the request is a JSON, it will be a dict. +body (Union[str, bytes]): The raw body of the request. For JSON payloads, use the `json()` method to parse the body into a dict with proper type preservation.
  • method (str): The method of the request. `e.g. GET, POST, PUT, DELETE` @@ -70,6 +70,33 @@ identity (Optional[Identity]): The identity of the client +## Parsing JSON Body + +The `request.json()` method parses the request body as JSON and returns a Python `dict` with full type preservation: + +- JSON `null` becomes Python `None` +- JSON numbers become Python `int` or `float` +- JSON booleans become Python `bool` +- JSON strings become Python `str` +- JSON arrays become Python `list` +- JSON objects become Python `dict` + +Nested structures are handled recursively up to a maximum depth of 128 levels. + + + +```python +@app.post("/example") +async def handler(request: Request): + data = request.json() # Returns a dict with preserved types + # e.g. {"count": 42, "active": true, "tags": ["a", "b"]} + # -> {"count": 42, "active": True, "tags": ["a", "b"]} + return {"received": data} +``` + + +If the body is not valid JSON or is not a JSON object, a `ValueError` will be raised. + ## Extra Path Parameters Robyn supports capturing extra path parameters using the `*extra` syntax in route definitions. This allows you to capture any additional segments in the URL path that come after the defined route. diff --git a/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx b/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx index 5c5157ce4..9467e25b4 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx @@ -23,7 +23,7 @@ headers (dict[str, str]):请求的标头。`例如:{"Content-Type": "applica params (dict[str, str]):请求的路径参数。`例如:/user/:id -> {"id": "123"}`
  • -body (Union[str, bytes]):请求的正文。如果请求是 JSON 格式,它就会被解析为字典为一个字典。 +body (Union[str, bytes]):请求的原始正文。对于 JSON 请求体,请使用 `json()` 方法将正文解析为具有正确类型保留的字典。
  • method (str):请求的方法。`例如:GET、POST、PUT、DELETE` @@ -63,6 +63,33 @@ identity (Optional[Identity]):客户端的身份 +## 解析 JSON 请求体 + +`request.json()` 方法将请求体解析为 JSON,并返回一个保留完整类型的 Python `dict` 或 `list`(JSON 对象返回 `dict`,JSON 数组返回 `list`): + +- JSON `null` 转换为 Python `None` +- JSON 数字转换为 Python `int` 或 `float` +- JSON 布尔值转换为 Python `bool` +- JSON 字符串转换为 Python `str` +- JSON 数组转换为 Python `list` +- JSON 对象转换为 Python `dict` + +嵌套结构将被递归处理,最大深度为 128 层。 + + + +```python +@app.post("/example") +async def handler(request: Request): + data = request.json() # 返回一个保留类型的字典或列表 + # 例如 {"count": 42, "active": true, "tags": ["a", "b"]} + # -> {"count": 42, "active": True, "tags": ["a", "b"]} + return {"received": data} +``` + + +如果请求体不是有效的 JSON,将会抛出 `ValueError`。 + ## Extra Path Parameters Robyn 支持通过 `*extra` 语法捕获额外的路径参数,这样可以捕获在定义的路由之后的所有额外的路径段。 diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 8e93f2106..5dfad74fe 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -11,7 +11,7 @@ from robyn.authentication import AuthenticationHandler, BearerGetter, Identity from robyn.robyn import QueryParams, Url from robyn.templating import JinjaTemplate -from robyn.types import Body, JSONResponse, Method, PathParams +from robyn.types import Body, JsonBody, JSONResponse, Method, PathParams app = Robyn(__file__) @@ -1225,6 +1225,47 @@ def create_item(request, body: CreateItemBody, query: CreateItemQueryParamsParam return CreateItemResponse(success=True, items_changed=2) +# ===== JsonBody Routes ===== + + +class TemperatureInput(JsonBody): + fahrenheit: float + + +@app.post("/sync/json_body/bare") +def sync_json_body_bare(data: JsonBody): + """Bare JsonBody - receives parsed JSON dict""" + return data + + +@app.post("/async/json_body/bare") +async def async_json_body_bare(data: JsonBody): + """Bare JsonBody - receives parsed JSON dict""" + return data + + +@app.post("/sync/json_body/typed") +def sync_json_body_typed(data: TemperatureInput): + """Typed JsonBody - receives parsed JSON dict, docs show schema""" + fahrenheit = data.get("fahrenheit", 0) + celsius = (float(fahrenheit) - 32) * 5 / 9 + return {"celsius": celsius} + + +@app.post("/async/json_body/typed") +async def async_json_body_typed(data: TemperatureInput): + """Typed JsonBody - receives parsed JSON dict, docs show schema""" + fahrenheit = data.get("fahrenheit", 0) + celsius = (float(fahrenheit) - 32) * 5 / 9 + return {"celsius": celsius} + + +@app.post("/openapi_json_body") +def openapi_json_body_endpoint(request: Request, data: TemperatureInput) -> dict: + """Convert fahrenheit to celsius using JsonBody""" + return {"celsius": (float(data.get("fahrenheit", 0)) - 32) * 5 / 9} + + # ===== Server-Sent Events (SSE) Routes ===== diff --git a/integration_tests/test_openapi.py b/integration_tests/test_openapi.py index 18de22afb..0e74475ad 100644 --- a/integration_tests/test_openapi.py +++ b/integration_tests/test_openapi.py @@ -214,3 +214,52 @@ def test_openapi_query_params(): assert "required" == openapi_spec["paths"][endpoint][route_type]["parameters"][0]["name"] assert "query" == openapi_spec["paths"][endpoint][route_type]["parameters"][0]["in"] assert {"type": "boolean"} == openapi_spec["paths"][endpoint][route_type]["parameters"][0]["schema"] + + +@pytest.mark.benchmark +def test_openapi_json_body_typed(): + """Test that a typed JsonBody subclass generates a proper requestBody schema in OpenAPI docs.""" + openapi_response = get("/openapi.json", should_check_response=False) + + assert openapi_response.status_code == 200 + + openapi_spec = openapi_response.json() + + assert isinstance(openapi_spec, dict) + + route_type = "post" + endpoint = "/openapi_json_body" + + assert endpoint in openapi_spec["paths"] + assert route_type in openapi_spec["paths"][endpoint] + assert "requestBody" in openapi_spec["paths"][endpoint][route_type] + assert "content" in openapi_spec["paths"][endpoint][route_type]["requestBody"] + assert "application/json" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"] + assert "schema" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"] + assert "properties" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"] + + properties = openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"] + assert "fahrenheit" in properties + assert "number" == properties["fahrenheit"]["type"] + + +@pytest.mark.benchmark +def test_openapi_json_body_bare(): + """Test that a bare JsonBody generates a requestBody with empty properties in OpenAPI docs.""" + openapi_response = get("/openapi.json", should_check_response=False) + + assert openapi_response.status_code == 200 + + openapi_spec = openapi_response.json() + + assert isinstance(openapi_spec, dict) + + route_type = "post" + # bare JsonBody routes should still have requestBody in the spec + endpoint = "/sync/json_body/bare" + + assert endpoint in openapi_spec["paths"] + assert route_type in openapi_spec["paths"][endpoint] + assert "requestBody" in openapi_spec["paths"][endpoint][route_type] + assert "content" in openapi_spec["paths"][endpoint][route_type]["requestBody"] + assert "application/json" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"] diff --git a/integration_tests/test_split_request_params.py b/integration_tests/test_split_request_params.py index 2b63a50a1..37afb10b3 100644 --- a/integration_tests/test_split_request_params.py +++ b/integration_tests/test_split_request_params.py @@ -1,6 +1,6 @@ import pytest -from integration_tests.helpers.http_methods_helpers import get, post +from integration_tests.helpers.http_methods_helpers import get, json_post, post @pytest.mark.benchmark @@ -83,3 +83,20 @@ def test_split_request_params_typed_untyped_post_combined(session, function_type def test_split_request_params_get_combined_failure(session, function_type): res = post(f"/{function_type}/split_request_typed_untyped/combined/failure?hello=robyn&a=1&b=2", data={"hello": "world"}, should_check_response=False) assert 500 == res.status_code + + +@pytest.mark.benchmark +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_body_bare(session, function_type): + """Test that bare JsonBody passes the parsed JSON dict to the handler.""" + res = json_post(f"/{function_type}/json_body/bare", json_data={"hello": "world", "count": 42}) + assert res.json() == {"hello": "world", "count": 42} + + +@pytest.mark.benchmark +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_json_body_typed(session, function_type): + """Test that typed JsonBody subclass passes the parsed JSON dict to the handler.""" + res = json_post(f"/{function_type}/json_body/typed", json_data={"fahrenheit": 212}) + result = res.json() + assert result["celsius"] == pytest.approx(100.0) diff --git a/robyn/__init__.py b/robyn/__init__.py index 45aea5d5d..cd9ccfa29 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -23,7 +23,7 @@ from robyn.responses import SSEMessage, SSEResponse, StreamingResponse, html, serve_file, serve_html from robyn.robyn import FunctionInfo, Headers, HttpMethod, Request, Response, WebSocketConnector, get_version from robyn.router import MiddlewareRouter, MiddlewareType, Router, WebSocketRouter -from robyn.types import Directory +from robyn.types import Directory, JsonBody from robyn.ws import WebSocket, WebSocketAdapter, WebSocketDisconnect, create_websocket_decorator __version__ = get_version() @@ -810,5 +810,6 @@ def cors_middleware(request): "WebSocket", "WebSocketAdapter", "WebSocketDisconnect", + "JsonBody", "MCPApp", ] diff --git a/robyn/openapi.py b/robyn/openapi.py index f3f189068..d3640441f 100644 --- a/robyn/openapi.py +++ b/robyn/openapi.py @@ -10,7 +10,7 @@ from robyn.responses import html from robyn.robyn import QueryParams, Response -from robyn.types import Body +from robyn.types import Body, JsonBody class str_typed_dict(TypedDict): @@ -205,7 +205,9 @@ def add_openapi_path_obj(self, route_type: str, endpoint: str, openapi_name: str param_annotation = parameters[parameter].annotation if inspect.isclass(param_annotation): - if issubclass(param_annotation, Body): + if issubclass(param_annotation, JsonBody): + request_body = param_annotation + elif issubclass(param_annotation, Body): request_body = param_annotation elif issubclass(param_annotation, QueryParams): query_params = param_annotation diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi index 9a7d2a9c6..554a85aa5 100644 --- a/robyn/robyn.pyi +++ b/robyn/robyn.pyi @@ -393,9 +393,20 @@ class Request: def json(self) -> Union[dict, list]: """ - If the body is valid JSON, this will return the parsed JSON data - as a dict (for JSON objects) or a list (for JSON arrays). - Otherwise, this will throw a ValueError. + Parse the request body as JSON and return a Python dict or list with preserved types. + + JSON types are mapped to Python types as follows: + - null -> None + - bool -> bool + - number -> int or float + - string -> str + - array -> list + - object -> dict + + Nested structures are handled recursively up to a maximum depth of 128. + + Raises: + ValueError: If the body is not valid JSON. """ pass diff --git a/robyn/router.py b/robyn/router.py index 3bbf3666f..a987039cf 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -12,7 +12,7 @@ from robyn.openapi import OpenAPI from robyn.responses import FileResponse, StreamingResponse from robyn.robyn import FunctionInfo, Headers, HttpMethod, Identity, MiddlewareType, QueryParams, Request, Response, Url -from robyn.types import Body, Files, FormData, IPAddress, Method, PathParams +from robyn.types import Body, Files, FormData, IPAddress, JsonBody, Method, PathParams _logger = logging.getLogger(__name__) @@ -157,7 +157,16 @@ def wrapped_handler(*args, **kwargs): elif handler_param_type is type_mapping[type_name]: type_filtered_params[handler_param_name] = getattr(request, type_name) elif inspect.isclass(handler_param_type): - if issubclass(handler_param_type, Body): + if issubclass(handler_param_type, JsonBody): + try: + type_filtered_params[handler_param_name] = request.json() + except ValueError as e: + return Response( + status_code=status_codes.HTTP_400_BAD_REQUEST, + headers=Headers({"Content-Type": "application/json"}), + description=jsonify({"error": f"Invalid JSON body: {e}"}), + ) + elif issubclass(handler_param_type, Body): type_filtered_params[handler_param_name] = getattr(request, "body") elif issubclass(handler_param_type, QueryParams): type_filtered_params[handler_param_name] = getattr(request, "query_params") diff --git a/robyn/types.py b/robyn/types.py index 071e62a60..38df4ed30 100644 --- a/robyn/types.py +++ b/robyn/types.py @@ -41,4 +41,39 @@ class Body: pass -__all__ = ["JSONResponse", "Body"] +class JsonBody: + """ + A type alias for JSON request bodies. When used as a parameter type annotation, + the handler receives the parsed JSON (dict) from request.json() and the OpenAPI + docs will show a generic JSON request body input. + + Can be subclassed with annotations to provide a typed schema in the OpenAPI docs: + + class MyBody(JsonBody): + name: str + age: int + + @app.post("/users") + def create_user(request: Request, data: MyBody): + # data is the parsed JSON dict + ... + + .. note:: + + The JSON body is parsed via ``request.json()`` during parameter + resolution, *before* the handler is invoked. If the request body is + not valid JSON, a 400 Bad Request response is returned automatically + with a JSON error message (e.g., ``{"error": "Invalid JSON body: ..."}``) + and the handler is never called. Because parsing happens before + handler invocation, the error **cannot** be caught with a try/except + inside the handler. + + If you need custom error handling for malformed JSON, accept the raw + body instead (e.g., ``body: Body``) and call ``request.json()`` + yourself inside a try/except block. + """ + + pass + + +__all__ = ["JSONResponse", "Body", "JsonBody"] From ae44bc0a3dfa5dcd840020e717fcb8942249a014 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:03:38 +0000 Subject: [PATCH 040/106] feat(websocket): make max payload configurable via env/robyn.env and document usage (#1318) --- .../en/api_reference/robyn_env.mdx | 2 +- .../zh/api_reference/robyn_env.mdx | 2 +- integration_tests/base_routes.py | 21 +++++++++++++++++++ integration_tests/test_web_sockets.py | 15 +++++++++++++ src/server.rs | 1 + src/websockets/mod.rs | 5 ++++- 6 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docs_src/src/pages/documentation/en/api_reference/robyn_env.mdx b/docs_src/src/pages/documentation/en/api_reference/robyn_env.mdx index 3f5998a6b..e4ae0571f 100644 --- a/docs_src/src/pages/documentation/en/api_reference/robyn_env.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/robyn_env.mdx @@ -19,7 +19,7 @@ Batman wanted to configure the server through an environment file. Changing code - `ROBYN_DEV_MODE`: Configures the dev mode - Default: `False` - Example: `ROBYN_DEV_MODE=True` - - `ROBYN_MAX_PAYLOAD_SIZE`: Sets the maximum payload size for requests in bytes. + - `ROBYN_MAX_PAYLOAD_SIZE`: Sets the maximum payload size for HTTP requests and WebSocket messages in bytes. - Default: `1000000` bytes - Example: `ROBYN_MAX_PAYLOAD_SIZE=1000000` diff --git a/docs_src/src/pages/documentation/zh/api_reference/robyn_env.mdx b/docs_src/src/pages/documentation/zh/api_reference/robyn_env.mdx index 0e0db9fd4..2f8bb780b 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/robyn_env.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/robyn_env.mdx @@ -18,7 +18,7 @@ export const description = '在本节中,我们将学习如何通过配置文 - `ROBYN_DEV_MODE`:是否开启开发者模式。 - 默认值:`False` - 示例:`ROBYN_DEV_MODE=True` -- `ROBYN_MAX_PAYLOAD_SIZE`:设置请求的最大负载大小(以字节为单位)。 +- `ROBYN_MAX_PAYLOAD_SIZE`:设置 HTTP 请求和 WebSocket 消息的最大负载大小(以字节为单位)。 - 默认值:1000000 bytes - 示例:`ROBYN_MAX_PAYLOAD_SIZE=1000000` diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 5dfad74fe..788dd0593 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -124,6 +124,27 @@ async def di_websocket_on_close(websocket, global_dependencies=None): return f"close: {global_dep}" +# --- WebSocket echo endpoint for large payload testing (#1269) --- +@app.websocket("/web_socket_echo") +async def echo_websocket_endpoint(websocket): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text(msg) + except WebSocketDisconnect: + pass + + +@echo_websocket_endpoint.on_connect +def echo_websocket_on_connect(websocket): + return "" + + +@echo_websocket_endpoint.on_close +def echo_websocket_on_close(websocket): + return "" + + # --- WebSocket with empty returns --- @app.websocket("/web_socket_empty_returns") async def empty_websocket_endpoint(websocket): diff --git a/integration_tests/test_web_sockets.py b/integration_tests/test_web_sockets.py index ea62466cd..05cf00e57 100644 --- a/integration_tests/test_web_sockets.py +++ b/integration_tests/test_web_sockets.py @@ -75,6 +75,21 @@ def test_websocket_di(session): ws.close() +def test_websocket_large_payload(session): + """Test that WebSocket can handle messages larger than the default 64KB frame size (#1269)""" + ws = create_connection(f"{BASE_URL}/web_socket_echo") + # Consume the empty connect message + ws.recv() + + large_message = "A" * (128 * 1024) # 128KB, well above the old 64KB default + ws.send(large_message) + response = ws.recv() + assert response == large_message + assert len(response) == 128 * 1024 + + ws.close() + + def test_websocket_empty_returns(session): """Test that WebSocket handlers can return nothing without causing errors""" ws = create_connection(f"{BASE_URL}/web_socket_empty_returns") diff --git a/src/server.rs b/src/server.rs index 324257ba0..2d926be83 100644 --- a/src/server.rs +++ b/src/server.rs @@ -212,6 +212,7 @@ impl Server { task_locals, endpoint_copy.to_string(), use_channel, + max_payload_size, ) }), ); diff --git a/src/websockets/mod.rs b/src/websockets/mod.rs index 21b42f577..495f425e5 100644 --- a/src/websockets/mod.rs +++ b/src/websockets/mod.rs @@ -301,6 +301,7 @@ pub async fn start_web_socket( task_locals: TaskLocals, endpoint: String, use_channel: bool, + max_frame_size: usize, ) -> Result { let registry_addr = get_or_init_registry_for_endpoint(endpoint); @@ -317,7 +318,7 @@ pub async fn start_web_socket( } } - ws::start( + ws::WsResponseBuilder::new( WebSocketConnector { router, task_locals, @@ -331,4 +332,6 @@ pub async fn start_web_socket( &req, stream, ) + .frame_size(max_frame_size) + .start() } From 979e3d8df0778999314322f6f66426a3ce637174 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sun, 22 Feb 2026 00:25:09 +0000 Subject: [PATCH 041/106] Release 0.79.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- docs_src/public/llms.txt | 2 +- llms.txt | 2 +- pyproject.toml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91c17a350..86bbd14c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.78.0" +version = "0.79.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index c496c0437..16d8bb604 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.78.0" +version = "0.79.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/docs_src/public/llms.txt b/docs_src/public/llms.txt index 19153884b..c64f04630 100644 --- a/docs_src/public/llms.txt +++ b/docs_src/public/llms.txt @@ -4,7 +4,7 @@ ## Quick Facts -- Version: 0.78.0 +- Version: 0.79.0 - Python: >= 3.10 - License: BSD 2.0 - Repository: https://github.com/sparckles/robyn diff --git a/llms.txt b/llms.txt index 19153884b..c64f04630 100644 --- a/llms.txt +++ b/llms.txt @@ -4,7 +4,7 @@ ## Quick Facts -- Version: 0.78.0 +- Version: 0.79.0 - Python: >= 3.10 - License: BSD 2.0 - Repository: https://github.com/sparckles/robyn diff --git a/pyproject.toml b/pyproject.toml index 4ecd02ecb..248ac85e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.78.0" +version = "0.79.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -67,7 +67,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.78.0" +version = "0.79.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 29c4d0f706531e3c058224ca8d2050589d94794d Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:50:27 +0000 Subject: [PATCH 042/106] feat: deprecate old websockets (#1320) * feat: deprecate old websockets * update * update --- docs_src/public/llms.txt | 27 +++--- .../api_reference/architecture_deep_dive.mdx | 19 ++-- .../en/api_reference/websockets.mdx | 33 ------- .../example_app/real_time_notifications.mdx | 29 ++++--- .../zh/api_reference/websockets.mdx | 33 ------- .../example_app/real_time_notifications.mdx | 29 ++++--- integration_tests/subroutes/__init__.py | 27 +++--- llms.txt | 27 +++--- robyn/__init__.py | 3 +- robyn/processpool.py | 16 +--- robyn/ws.py | 86 +++---------------- src/executors/web_socket_executors.rs | 71 ++------------- src/routers/web_socket_router.rs | 16 ---- src/server.rs | 13 +-- src/websockets/mod.rs | 57 ++++-------- 15 files changed, 128 insertions(+), 358 deletions(-) diff --git a/docs_src/public/llms.txt b/docs_src/public/llms.txt index c64f04630..8e9414a1a 100644 --- a/docs_src/public/llms.txt +++ b/docs_src/public/llms.txt @@ -141,20 +141,23 @@ Response( ### WebSockets ```python -from robyn import WebSocket - -ws = WebSocket(app, "/ws") - -@ws.on("connect") -def on_connect(ws, msg): +from robyn import WebSocketDisconnect + +@app.websocket("/ws") +async def handler(websocket): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") + except WebSocketDisconnect: + pass + +@handler.on_connect +def on_connect(websocket): return "Connected" -@ws.on("message") -def on_message(ws, msg): - return f"Echo: {msg}" - -@ws.on("close") -def on_close(ws, msg): +@handler.on_close +def on_close(websocket): return "Closed" ``` diff --git a/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx b/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx index 643639f2f..e7cd19473 100644 --- a/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx @@ -310,16 +310,17 @@ Robyn's WebSocket implementation maintains persistent connections in the Rust la ```python - from robyn import WebSocket - + from robyn import WebSocketDisconnect + @app.websocket("/chat") - async def websocket_handler(websocket: WebSocket): - # Connection established in Rust - # Message handling in Python - async for message in websocket: - # Process message - response = process_chat_message(message) - await websocket.send_text(response) + async def websocket_handler(websocket): + try: + while True: + message = await websocket.receive_text() + response = process_chat_message(message) + await websocket.send_text(response) + except WebSocketDisconnect: + pass ``` diff --git a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx index 6258c1e63..7b17f9108 100644 --- a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx @@ -265,39 +265,6 @@ To handle real-time bidirectional communication, Batman learned how to work with --- -## Legacy API {{ tag: 'Legacy', label: 'Legacy' }} - - - - The old event-based WebSocket API is still supported for backward compatibility. If you have existing code using `WebSocket(app, "/ws")` with `@websocket.on("message")`, it will continue to work without changes. - - - - - ```python {{ title: 'Legacy Style' }} - from robyn import Robyn, WebSocket - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("connect") - def connect(): - return "Hello world, from ws" - - @websocket.on("message") - def message(ws, msg): - return f"Echo: {msg}" - - @websocket.on("close") - def close(): - return "Goodbye world, from ws" - ``` - - - - ---- - ## What's next? As the codebase grew, Batman wanted to onboard the justice league to help him manage the application. diff --git a/docs_src/src/pages/documentation/en/example_app/real_time_notifications.mdx b/docs_src/src/pages/documentation/en/example_app/real_time_notifications.mdx index 280e889fe..48e1acc35 100644 --- a/docs_src/src/pages/documentation/en/example_app/real_time_notifications.mdx +++ b/docs_src/src/pages/documentation/en/example_app/real_time_notifications.mdx @@ -6,21 +6,24 @@ Batman decided to implement real-time notifications for police officers using We -```python {{ title: 'Setting up Authentication Middlewares' }} -from robyn import WebSocket - -websocket = WebSocket(app, "/notifications") - -@websocket.on("connect") -async def notify_connect(): +```python {{ title: 'Setting up Real-time Notifications' }} +from robyn import WebSocketDisconnect + +@app.websocket("/notifications") +async def notify_handler(websocket): + try: + while True: + message = await websocket.receive_text() + await websocket.send_text(f"Received: {message}") + except WebSocketDisconnect: + pass + +@notify_handler.on_connect +def notify_connect(websocket): return "Connected to notifications" -@websocket.on("message") -async def notify_message(message): - return f"Received: {message}" - -@websocket.on("close") -async def notify_close(): +@notify_handler.on_close +def notify_close(websocket): return "Disconnected from notifications" ``` diff --git a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx index 02667635c..7311105a9 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx @@ -262,39 +262,6 @@ export const description = --- -## 旧版 API {{ tag: '旧版', label: '旧版' }} - - - - 旧版基于事件的 WebSocket API 仍然支持向后兼容。如果您有使用 `WebSocket(app, "/ws")` 和 `@websocket.on("message")` 的现有代码,它将继续正常工作。 - - - - - ```python {{ title: '旧版风格' }} - from robyn import Robyn, WebSocket - - app = Robyn(__file__) - websocket = WebSocket(app, "/web_socket") - - @websocket.on("connect") - def connect(): - return "Hello world, from ws" - - @websocket.on("message") - def message(ws, msg): - return f"Echo: {msg}" - - @websocket.on("close") - def close(): - return "Goodbye world, from ws" - ``` - - - - ---- - ## 下一步 随着代码库的扩展,蝙蝠侠希望正义联盟的成员能够参与管理应用程序。 diff --git a/docs_src/src/pages/documentation/zh/example_app/real_time_notifications.mdx b/docs_src/src/pages/documentation/zh/example_app/real_time_notifications.mdx index 3bc79e512..43f394eed 100644 --- a/docs_src/src/pages/documentation/zh/example_app/real_time_notifications.mdx +++ b/docs_src/src/pages/documentation/zh/example_app/real_time_notifications.mdx @@ -5,21 +5,24 @@ export const description = 蝙蝠侠决定使用 WebSockets 为哥谭市警察局的警官们实现实时通知功能,从而使他们能够即时接收关于犯罪活动的更新,并在发生新的犯罪报告时触发警报。 -```python {{ title: '设置身份验证中间件' }} -from robyn import WebSocket - -websocket = WebSocket(app, "/notifications") - -@websocket.on("connect") -async def notify_connect(): +```python {{ title: '设置实时通知' }} +from robyn import WebSocketDisconnect + +@app.websocket("/notifications") +async def notify_handler(websocket): + try: + while True: + message = await websocket.receive_text() + await websocket.send_text(f"Received: {message}") + except WebSocketDisconnect: + pass + +@notify_handler.on_connect +def notify_connect(websocket): return "Connected to notifications" -@websocket.on("message") -async def notify_message(message): - return f"Received: {message}" - -@websocket.on("close") -async def notify_close(): +@notify_handler.on_close +def notify_close(websocket): return "Disconnected from notifications" ``` diff --git a/integration_tests/subroutes/__init__.py b/integration_tests/subroutes/__init__.py index ec10298c1..3f5b8db5e 100644 --- a/integration_tests/subroutes/__init__.py +++ b/integration_tests/subroutes/__init__.py @@ -1,27 +1,30 @@ -from robyn import SubRouter, WebSocket, jsonify +from robyn import SubRouter, WebSocketDisconnect, jsonify from .di_subrouter import di_subrouter from .file_api import static_router sub_router = SubRouter(__name__, prefix="/sub_router") -websocket = WebSocket(sub_router, "/ws") +__all__ = ["sub_router", "di_subrouter", "static_router"] -__all__ = ["sub_router", "websocket", "di_subrouter", "static_router"] - -@websocket.on("connect") -async def connect(ws): - return "Hello world, from ws" +@sub_router.websocket("/ws") +async def ws_handler(websocket): + try: + while True: + await websocket.receive_text() + await websocket.send_text("Message") + except WebSocketDisconnect: + pass -@websocket.on("message") -async def message(): - return "Message" +@ws_handler.on_connect +async def connect(websocket): + return "Hello world, from ws" -@websocket.on("close") -async def close(ws): +@ws_handler.on_close +async def close(websocket): return jsonify({"message": "closed"}) diff --git a/llms.txt b/llms.txt index c64f04630..8e9414a1a 100644 --- a/llms.txt +++ b/llms.txt @@ -141,20 +141,23 @@ Response( ### WebSockets ```python -from robyn import WebSocket - -ws = WebSocket(app, "/ws") - -@ws.on("connect") -def on_connect(ws, msg): +from robyn import WebSocketDisconnect + +@app.websocket("/ws") +async def handler(websocket): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"Echo: {msg}") + except WebSocketDisconnect: + pass + +@handler.on_connect +def on_connect(websocket): return "Connected" -@ws.on("message") -def on_message(ws, msg): - return f"Echo: {msg}" - -@ws.on("close") -def on_close(ws, msg): +@handler.on_close +def on_close(websocket): return "Closed" ``` diff --git a/robyn/__init__.py b/robyn/__init__.py index cd9ccfa29..08dd0631d 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -24,7 +24,7 @@ from robyn.robyn import FunctionInfo, Headers, HttpMethod, Request, Response, WebSocketConnector, get_version from robyn.router import MiddlewareRouter, MiddlewareType, Router, WebSocketRouter from robyn.types import Directory, JsonBody -from robyn.ws import WebSocket, WebSocketAdapter, WebSocketDisconnect, create_websocket_decorator +from robyn.ws import WebSocketAdapter, WebSocketDisconnect, create_websocket_decorator __version__ = get_version() @@ -807,7 +807,6 @@ def cors_middleware(request): "AuthenticationHandler", "Headers", "WebSocketConnector", - "WebSocket", "WebSocketAdapter", "WebSocketDisconnect", "JsonBody", diff --git a/robyn/processpool.py b/robyn/processpool.py index 4b4efac1a..e1ca456f3 100644 --- a/robyn/processpool.py +++ b/robyn/processpool.py @@ -210,21 +210,11 @@ def spawn_process( for endpoint in web_sockets: web_socket = web_sockets[endpoint] - # Support both old-style WebSocket objects and new-style handler dicts - if hasattr(web_socket, "methods"): - # Old-style: WebSocket class with .methods dict - methods = web_socket.methods - use_channel = False - else: - # New-style: plain dict of handlers - methods = web_socket - use_channel = web_socket.get("_use_channel", False) server.add_web_socket_route( endpoint, - methods["connect"], - methods["close"], - methods["message"], - use_channel, + web_socket["connect"], + web_socket["close"], + web_socket["message"], ) try: diff --git a/robyn/ws.py b/robyn/ws.py index e85cfeba1..be2d7b5f7 100644 --- a/robyn/ws.py +++ b/robyn/ws.py @@ -3,17 +3,11 @@ import asyncio import inspect import logging -from typing import TYPE_CHECKING, Callable, Dict import orjson -from robyn.argument_parser import Config -from robyn.dependency_injection import DependencyMap from robyn.robyn import FunctionInfo, WebSocketConnector -if TYPE_CHECKING: - from robyn import Robyn - _logger = logging.getLogger(__name__) @@ -54,10 +48,9 @@ async def receive_bytes(self) -> bytes: return text.encode("utf-8") async def receive_json(self): - """Receive and decode JSON data.""" + """Receive and decode JSON data. + Raises WebSocketDisconnect when the connection is closed.""" text = await self.receive_text() - if text is None: - return None return orjson.loads(text) async def send_text(self, data: str): @@ -92,7 +85,7 @@ def query_params(self): # Global storage for connection state (per-connection queues and tasks) -_connection_tasks: Dict[str, asyncio.Task] = {} +_connection_tasks: dict[str, asyncio.Task] = {} def create_websocket_decorator(app_instance): @@ -157,11 +150,10 @@ async def _run_handler(): await handler(adapter, **di_kwargs) except WebSocketDisconnect: pass - except Exception as e: - if "connection closed" in str(e).lower() or "websocket" in str(e).lower(): - pass - else: - _logger.exception("Error in WebSocket handler for %s: %s", endpoint, e) + except ConnectionError: + _logger.debug("Connection lost in WebSocket handler for %s", endpoint, exc_info=True) + except Exception: + _logger.exception("Error in WebSocket handler for %s", endpoint) finally: _connection_tasks.pop(conn_id, None) @@ -198,7 +190,11 @@ async def close_handler(ws): if task is not None: try: await asyncio.wait_for(task, timeout=5.0) - except (asyncio.TimeoutError, asyncio.CancelledError, Exception): + except (asyncio.TimeoutError, asyncio.CancelledError): + if not task.done(): + task.cancel() + except Exception: + _logger.debug("Unexpected error while awaiting handler task for %s", conn_id, exc_info=True) if not task.done(): task.cancel() @@ -247,9 +243,6 @@ async def close_handler(ws): {}, ) - # Mark as channel-based - handlers["_use_channel"] = True - # --- Decorator methods for on_connect / on_close --- def add_on_connect(connect_fn): nonlocal _on_connect_fn @@ -272,58 +265,3 @@ def add_on_close(close_fn): return decorator return websocket - - -class WebSocket: - """Legacy WebSocket class for backward compatibility. - - Uses the old event-based API with @websocket.on("connect"/"message"/"close"). - """ - - def __init__(self, robyn_object: "Robyn", endpoint: str, config: Config = Config(), dependencies: DependencyMap = DependencyMap()) -> None: - self.robyn_object = robyn_object - self.endpoint = endpoint - self.methods: dict = {} - self.config = config - self.dependencies = dependencies - - def on(self, type: str) -> Callable[..., None]: - def inner(handler): - if type not in ["connect", "close", "message"]: - raise Exception(f"Socket method {type} does not exist") - - params = dict(inspect.signature(handler).parameters) - num_params = len(params) - is_async = inspect.iscoroutinefunction(handler) - - injected_dependencies = self.dependencies.get_dependency_map(self) - - new_injected_dependencies = {} - if "global_dependencies" in params: - new_injected_dependencies["global_dependencies"] = injected_dependencies.get("global_dependencies", {}) - if "router_dependencies" in params: - new_injected_dependencies["router_dependencies"] = injected_dependencies.get("router_dependencies", {}) - - self.methods[type] = FunctionInfo(handler, is_async, num_params, params, new_injected_dependencies) - self.robyn_object.add_web_socket(self.endpoint, self) - - return handler - - return inner - - def inject(self, **kwargs): - """ - Injects the dependencies for the route - - :param kwargs dict: the dependencies to be injected - """ - self.dependencies.add_router_dependency(self, **kwargs) - - def inject_global(self, **kwargs): - """ - Injects the dependencies for the global routes - Ideally, this function should be a global function - - :param kwargs dict: the dependencies to be injected - """ - self.dependencies.add_global_dependency(**kwargs) diff --git a/src/executors/web_socket_executors.rs b/src/executors/web_socket_executors.rs index 83cf95bd9..0df8d66b2 100644 --- a/src/executors/web_socket_executors.rs +++ b/src/executors/web_socket_executors.rs @@ -6,81 +6,18 @@ use pyo3_async_runtimes::TaskLocals; use crate::types::function_info::FunctionInfo; use crate::websockets::WebSocketConnector; -fn get_function_output<'a>( - function: &'a FunctionInfo, - fn_msg: Option, - py: Python<'a>, - ws: &WebSocketConnector, -) -> Result, PyErr> { - let handler = function.handler.bind(py).downcast()?; - - // this makes the request object accessible across every route - - let args = function.args.bind(py).downcast()?; - let kwargs = function.kwargs.bind(py).downcast()?; - - match function.number_of_params { - 0 => handler.call0(), - 1 => { - if pyo3::types::PyDictMethods::get_item(args, "ws").is_ok_and(|it| !it.is_none()) { - handler.call1((ws.clone(),)) - } else if pyo3::types::PyDictMethods::get_item(args, "msg") - .is_ok_and(|it| !it.is_none()) - { - handler.call1((fn_msg.unwrap_or_default(),)) - } else { - handler.call((), Some(kwargs)) - } - } - 2 => { - if pyo3::types::PyDictMethods::get_item(args, "ws").is_ok_and(|it| !it.is_none()) - && pyo3::types::PyDictMethods::get_item(args, "msg").is_ok_and(|it| !it.is_none()) - { - handler.call1((ws.clone(), fn_msg.unwrap_or_default())) - } else if pyo3::types::PyDictMethods::get_item(args, "ws").is_ok_and(|it| !it.is_none()) - { - handler.call((ws.clone(),), Some(kwargs)) - } else if pyo3::types::PyDictMethods::get_item(args, "msg") - .is_ok_and(|it| !it.is_none()) - { - handler.call((fn_msg.unwrap_or_default(),), Some(kwargs)) - } else { - handler.call((), Some(kwargs)) - } - } - 3 => { - if pyo3::types::PyDictMethods::get_item(args, "ws").is_ok_and(|it| !it.is_none()) - && pyo3::types::PyDictMethods::get_item(args, "msg").is_ok_and(|it| !it.is_none()) - { - handler.call((ws.clone(), fn_msg.unwrap_or_default()), Some(kwargs)) - } else if pyo3::types::PyDictMethods::get_item(args, "ws").is_ok_and(|it| !it.is_none()) - { - handler.call((ws.clone(),), Some(kwargs)) - } else if pyo3::types::PyDictMethods::get_item(args, "msg") - .is_ok_and(|it| !it.is_none()) - { - handler.call((fn_msg.unwrap_or_default(),), Some(kwargs)) - } else { - handler.call((), Some(kwargs)) - } - } - 4_u8..=u8::MAX => handler.call((ws.clone(), fn_msg.unwrap_or_default()), Some(kwargs)), - } -} - pub fn execute_ws_function( function: &FunctionInfo, - text: Option, task_locals: &TaskLocals, ctx: &mut WebsocketContext, ws: &WebSocketConnector, - // add number of params here ) { if function.is_async { let fut = Python::with_gil(|py| { + let handler = function.handler.bind(py).downcast().unwrap(); pyo3_async_runtimes::into_future_with_locals( task_locals, - get_function_output(function, text, py, ws).unwrap(), + handler.call1((ws.clone(),)).unwrap(), ) .unwrap() }); @@ -97,7 +34,9 @@ pub fn execute_ws_function( ctx.spawn(f); } else { Python::with_gil(|py| { - if let Some(op) = get_function_output(function, text, py, ws) + let handler = function.handler.bind(py).downcast().unwrap(); + if let Some(op) = handler + .call1((ws.clone(),)) .unwrap() .extract::>() .unwrap() diff --git a/src/routers/web_socket_router.rs b/src/routers/web_socket_router.rs index 8ac3040d7..c93611a5b 100644 --- a/src/routers/web_socket_router.rs +++ b/src/routers/web_socket_router.rs @@ -7,19 +7,15 @@ use crate::types::function_info::FunctionInfo; /// Contains the thread safe hashmaps of different routes type WebSocketRoutes = RwLock>>; -/// Tracks which endpoints use the new channel-based message delivery -type WebSocketChannelFlags = RwLock>; pub struct WebSocketRouter { web_socket_routes: WebSocketRoutes, - channel_flags: WebSocketChannelFlags, } impl WebSocketRouter { pub fn new() -> Self { Self { web_socket_routes: RwLock::new(HashMap::new()), - channel_flags: RwLock::new(HashMap::new()), } } @@ -28,20 +24,12 @@ impl WebSocketRouter { &self.web_socket_routes } - #[inline] - pub fn get_channel_flags(&self) -> &WebSocketChannelFlags { - &self.channel_flags - } - - // Checks if the functions is an async function - // Inserts them in the router according to their nature(CoRoutine/SyncFunction) pub fn add_websocket_route( &self, route: &str, connect_route: FunctionInfo, close_route: FunctionInfo, message_route: FunctionInfo, - use_channel: bool, ) { let table = self.get_web_socket_map(); @@ -58,9 +46,5 @@ impl WebSocketRouter { insert_in_router(connect_route, "connect"); insert_in_router(close_route, "close"); insert_in_router(message_route, "message"); - - self.channel_flags - .write() - .insert(route.to_string(), use_channel); } } diff --git a/src/server.rs b/src/server.rs index 2d926be83..b4a9e3f68 100644 --- a/src/server.rs +++ b/src/server.rs @@ -193,12 +193,10 @@ impl Server { .app_data(web::Data::new(excluded_response_headers_paths.clone())); let web_socket_map = web_socket_router.get_web_socket_map(); - let channel_flags = web_socket_router.get_channel_flags(); for (elem, value) in (web_socket_map.read()).iter() { let endpoint = elem.clone(); let path_params = value.clone(); let endpoint_for_closure = endpoint.clone(); - let use_channel = *channel_flags.read().get(&endpoint).unwrap_or(&false); app = app.route( &endpoint, web::get().to(move |stream: web::Payload, req: HttpRequest| { @@ -211,7 +209,6 @@ impl Server { path_params.clone(), task_locals, endpoint_copy.to_string(), - use_channel, max_payload_size, ) }), @@ -447,15 +444,9 @@ impl Server { connect_route: FunctionInfo, close_route: FunctionInfo, message_route: FunctionInfo, - use_channel: bool, ) { - self.websocket_router.add_websocket_route( - route, - connect_route, - close_route, - message_route, - use_channel, - ); + self.websocket_router + .add_websocket_route(route, connect_route, close_route, message_route); } /// Add a new startup handler diff --git a/src/websockets/mod.rs b/src/websockets/mod.rs index 495f425e5..89be7ddf3 100644 --- a/src/websockets/mod.rs +++ b/src/websockets/mod.rs @@ -54,8 +54,6 @@ pub struct WebSocketConnector { pub task_locals: TaskLocals, pub registry_addr: Addr, pub query_params: QueryParams, - /// Whether this connection uses the new channel-based message delivery. - pub use_channel: bool, /// Sender side of the message channel (stays in the Actix actor). pub message_sender: Option>>, /// Receiver side exposed to Python via WebSocketChannel. @@ -68,31 +66,27 @@ impl Actor for WebSocketConnector { fn started(&mut self, ctx: &mut Self::Context) { let addr = ctx.address(); - // Register with global registry self.registry_addr.do_send(Register { id: self.id, addr: addr.clone(), }); - // If new-style (channel mode), create the tokio channel - if self.use_channel { - let (tx, rx) = mpsc::unbounded_channel::>(); - self.message_sender = Some(tx); - self.message_channel = Python::with_gil(|py| { - Some( - Py::new( - py, - WebSocketChannel { - receiver: Arc::new(tokio::sync::Mutex::new(rx)), - }, - ) - .unwrap(), + let (tx, rx) = mpsc::unbounded_channel::>(); + self.message_sender = Some(tx); + self.message_channel = Python::with_gil(|py| { + Some( + Py::new( + py, + WebSocketChannel { + receiver: Arc::new(tokio::sync::Mutex::new(rx)), + }, ) - }); - } + .unwrap(), + ) + }); let function = self.router.get("connect").unwrap(); - execute_ws_function(function, None, &self.task_locals, ctx, self); + execute_ws_function(function, &self.task_locals, ctx, self); debug!("Actor is alive"); } @@ -104,7 +98,7 @@ impl Actor for WebSocketConnector { self.message_sender.take(); let function = self.router.get("close").unwrap(); - execute_ws_function(function, None, &self.task_locals, ctx, self); + execute_ws_function(function, &self.task_locals, ctx, self); debug!("Actor is dead"); } } @@ -119,7 +113,6 @@ impl Clone for WebSocketConnector { task_locals: task_locals_clone, registry_addr: self.registry_addr.clone(), query_params: self.query_params.clone(), - use_channel: self.use_channel, message_sender: self.message_sender.clone(), message_channel: Python::with_gil(|py| { self.message_channel.as_ref().map(|c| c.clone_ref(py)) @@ -156,27 +149,16 @@ impl StreamHandler> for WebSocketConnecto Ok(ws::Message::Text(text)) => { debug!("Text message received {:?}", text); if let Some(ref sender) = self.message_sender { - // New-style: push to Rust channel. No GIL. No Python call. let _ = sender.send(Some(text.to_string())); - } else { - // Old-style: call Python message handler directly - let function = self.router.get("message").unwrap(); - execute_ws_function( - function, - Some(text.to_string()), - &self.task_locals, - ctx, - self, - ); } } Ok(ws::Message::Binary(bin)) => ctx.binary(bin), Ok(ws::Message::Close(_close_reason)) => { debug!("Socket was closed"); - // Drop sender to signal channel closure + // Drop sender to signal channel closure so receive() returns None. + // The close handler is called once from stopped(). self.message_sender.take(); - let function = self.router.get("close").unwrap(); - execute_ws_function(function, None, &self.task_locals, ctx, self); + ctx.stop(); } _ => (), } @@ -263,8 +245,7 @@ impl WebSocketConnector { self.query_params.clone() } - /// Get the message channel for new-style WebSocket handlers. - /// Returns None for old-style handlers. + /// Get the message channel for WebSocket handlers. #[getter] pub fn get_message_channel(&self, py: Python) -> Option> { self.message_channel.as_ref().map(|c| c.clone_ref(py)) @@ -300,7 +281,6 @@ pub async fn start_web_socket( router: HashMap, task_locals: TaskLocals, endpoint: String, - use_channel: bool, max_frame_size: usize, ) -> Result { let registry_addr = get_or_init_registry_for_endpoint(endpoint); @@ -325,7 +305,6 @@ pub async fn start_web_socket( id: Uuid::new_v4(), registry_addr, query_params, - use_channel, message_sender: None, message_channel: None, }, From dac9223290b80249caf75e67705b926429175b7f Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:59:46 +0000 Subject: [PATCH 043/106] feat: add easy access query/path params with type coercion for HTTP and WebSocket handlers (#1309) * feat: add easy access query/path params with type coercion for HTTP and WebSocket handlers * update * update docs * update --- docs_src/public/llms.txt | 29 +++ .../en/api_reference/request_object.mdx | 94 +++++++++ .../en/api_reference/websockets.mdx | 39 ++++ .../zh/api_reference/request_object.mdx | 94 +++++++++ .../zh/api_reference/websockets.mdx | 39 ++++ integration_tests/base_routes.py | 86 +++++++- integration_tests/test_easy_access_params.py | 152 ++++++++++++++ .../test_split_request_params.py | 4 +- llms.txt | 29 +++ robyn/_param_utils.py | 191 ++++++++++++++++++ robyn/router.py | 43 +++- robyn/types.py | 4 +- robyn/ws.py | 93 +++++++-- 13 files changed, 872 insertions(+), 25 deletions(-) create mode 100644 integration_tests/test_easy_access_params.py create mode 100644 robyn/_param_utils.py diff --git a/docs_src/public/llms.txt b/docs_src/public/llms.txt index 8e9414a1a..82ddc93b8 100644 --- a/docs_src/public/llms.txt +++ b/docs_src/public/llms.txt @@ -48,6 +48,7 @@ app.start(port=8080) - **Authentication**: AuthenticationHandler base class for custom auth - **Static Files**: Directory serving via `app.serve_directory()` - **SSE**: Server-Sent Events support via `SSEResponse` +- **Easy Access Parameters**: Typed path/query params with automatic coercion in handler signatures - **Direct Rust Integration**: Embed Rust code directly in routes ## Project Structure @@ -161,6 +162,34 @@ def on_close(websocket): return "Closed" ``` +### Easy Access Parameters +Declare typed path and query parameters directly in handler signatures. Works for both HTTP and WebSocket handlers. + +```python +from typing import List, Optional + +# HTTP: path params + query params with type coercion +@app.get("/items/:id") +async def get_item(id: int, q: str, page: int = 1): + return {"id": id, "q": q, "page": page} + +# Optional, List, and bool params +@app.get("/search") +def search(name: str, tags: List[str], active: bool = False, age: Optional[int] = None): + return {"name": name, "tags": tags, "active": active, "age": age} + +# WebSocket: typed query params on handler and callbacks +@app.websocket("/ws") +async def handler(websocket, room: str = "default", page: int = 1): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"room={room} page={page} msg={msg}") + +@handler.on_connect +def on_connect(websocket, room: str = "default"): + return f"connected to {room}" +``` + ### MCP (Model Context Protocol) ```python @app.mcp.resource("time://current") diff --git a/docs_src/src/pages/documentation/en/api_reference/request_object.mdx b/docs_src/src/pages/documentation/en/api_reference/request_object.mdx index d5f5fce9c..1cc5dd6ff 100644 --- a/docs_src/src/pages/documentation/en/api_reference/request_object.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/request_object.mdx @@ -129,6 +129,100 @@ This feature is particularly useful when you need to handle dynamic, nested rout --- +## Easy Access Parameters + +Instead of manually extracting and converting query parameters and path parameters from the request object, you can declare them directly in your function signature with type annotations. Robyn will automatically resolve and coerce them for you. + +Any handler parameter that doesn't match a known request component (`Request`, `QueryParams`, `Headers`, etc.) is treated as an individual path or query parameter. + + + + **Basic usage** — path params and query params with type coercion and defaults. + + + + + ```python {{ title: 'Typed Params' }} + @app.get("/items/:id") + async def get_item(id: int, q: str, page: int = 1): + # id is coerced from the path param string to int + # q is taken from ?q=... + # page defaults to 1 if not provided + return {"id": id, "q": q, "page": page} + ``` + + ```python {{ title: 'Mixed with Request' }} + @app.get("/items/:id") + async def get_item(request: Request, id: int, q: str = ""): + # request is still injected as usual + # id and q are resolved as individual params + return {"id": id, "q": q, "method": request.method} + ``` + + + + + + + **Optional, List, Bool, and Float params** — Robyn handles common Python types automatically. + + - `Optional[T]` — resolves to `None` when not provided + - `List[T]` — collects repeated query params (e.g. `?tag=a&tag=b`) + - `bool` — accepts `true/false`, `1/0`, `yes/no`, `on/off` + - `float` — standard float coercion + + + + + ```python {{ title: 'Optional' }} + @app.get("/search") + def search(name: str, age: Optional[int] = None): + return {"name": name, "age": age} + # GET /search?name=bob -> {"name": "bob", "age": null} + # GET /search?name=bob&age=30 -> {"name": "bob", "age": 30} + ``` + + ```python {{ title: 'List' }} + from typing import List + + @app.get("/filter") + def filter_items(tag: List[str]): + return {"tags": tag} + # GET /filter?tag=python&tag=rust -> {"tags": ["python", "rust"]} + ``` + + ```python {{ title: 'Bool & Float' }} + @app.get("/settings") + def settings(active: bool = False, price: float = 0.0): + return {"active": active, "price": price} + # GET /settings?active=true&price=19.99 + # -> {"active": true, "price": 19.99} + ``` + + + + + + + **Error handling** — if a required parameter is missing or a value cannot be coerced to the declared type, Robyn returns a `400 Bad Request` response automatically. + + + + + ```python {{ title: 'Automatic 400' }} + @app.get("/items/:id") + def get_item(id: int, q: str): + return {"id": id, "q": q} + + # GET /items/42 -> 400 (missing required 'q') + # GET /items/abc?q=test -> 400 (cannot coerce 'abc' to int) + ``` + + + + +--- + ## What's next? diff --git a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx index 7b17f9108..39eeaf737 100644 --- a/docs_src/src/pages/documentation/en/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/websockets.mdx @@ -179,6 +179,45 @@ To handle real-time bidirectional communication, Batman learned how to work with --- +## Easy Access Query Parameters {{ tag: 'easy_access', label: 'easy_access' }} + + + + Instead of manually calling `websocket.query_params.get(...)`, you can declare typed query parameters directly in your handler, `on_connect`, and `on_close` signatures. Robyn will automatically resolve and coerce them — just like HTTP easy access parameters. + + Parameters with defaults are optional. Parameters without defaults are required — if missing, the connection is rejected with an error message. + + + + + ```python {{ title: 'Handler' }} + @app.websocket("/ws") + async def handler(websocket, room: str = "default", page: int = 1): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text( + f"room={room} page={page} msg={msg}" + ) + except WebSocketDisconnect: + pass + ``` + + ```python {{ title: 'Callbacks' }} + @handler.on_connect + def on_connect(websocket, room: str = "default"): + return f"connected to {room}" + + @handler.on_close + def on_close(websocket, room: str = "default"): + return f"left {room}" + ``` + + + + +--- + ## Closing Connections {{ tag: 'close', label: 'close' }} diff --git a/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx b/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx index 9467e25b4..b3345947f 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/request_object.mdx @@ -117,6 +117,100 @@ Robyn 支持通过 `*extra` 语法捕获额外的路径参数,这样可以捕 --- +## 便捷参数访问 + +你可以在函数签名中直接声明带类型注解的查询参数和路径参数,无需手动从请求对象中提取和转换。Robyn 会自动解析并进行类型转换。 + +任何不匹配已知请求组件(`Request`、`QueryParams`、`Headers` 等)的处理函数参数,都会被视为独立的路径参数或查询参数。 + + + + **基本用法** — 带类型转换和默认值的路径参数与查询参数。 + + + + + ```python {{ title: '类型化参数' }} + @app.get("/items/:id") + async def get_item(id: int, q: str, page: int = 1): + # id 从路径参数字符串自动转换为 int + # q 从 ?q=... 获取 + # page 未提供时默认为 1 + return {"id": id, "q": q, "page": page} + ``` + + ```python {{ title: '与 Request 混合使用' }} + @app.get("/items/:id") + async def get_item(request: Request, id: int, q: str = ""): + # request 仍然按原来的方式注入 + # id 和 q 作为独立参数解析 + return {"id": id, "q": q, "method": request.method} + ``` + + + + + + + **Optional、List、Bool 和 Float 参数** — Robyn 自动处理常见的 Python 类型。 + + - `Optional[T]` — 未提供时解析为 `None` + - `List[T]` — 收集重复的查询参数(例如 `?tag=a&tag=b`) + - `bool` — 接受 `true/false`、`1/0`、`yes/no`、`on/off` + - `float` — 标准浮点数转换 + + + + + ```python {{ title: 'Optional' }} + @app.get("/search") + def search(name: str, age: Optional[int] = None): + return {"name": name, "age": age} + # GET /search?name=bob -> {"name": "bob", "age": null} + # GET /search?name=bob&age=30 -> {"name": "bob", "age": 30} + ``` + + ```python {{ title: 'List' }} + from typing import List + + @app.get("/filter") + def filter_items(tag: List[str]): + return {"tags": tag} + # GET /filter?tag=python&tag=rust -> {"tags": ["python", "rust"]} + ``` + + ```python {{ title: 'Bool 和 Float' }} + @app.get("/settings") + def settings(active: bool = False, price: float = 0.0): + return {"active": active, "price": price} + # GET /settings?active=true&price=19.99 + # -> {"active": true, "price": 19.99} + ``` + + + + + + + **错误处理** — 如果缺少必需的参数或值无法转换为声明的类型,Robyn 会自动返回 `400 Bad Request` 响应。 + + + + + ```python {{ title: '自动返回 400' }} + @app.get("/items/:id") + def get_item(id: int, q: str): + return {"id": id, "q": q} + + # GET /items/42 -> 400(缺少必需的 'q') + # GET /items/abc?q=test -> 400(无法将 'abc' 转换为 int) + ``` + + + + +--- + ## 下一步 接下来,蝙蝠侠希望了解 Robyn 服务器的配置。于是他开始了解 Robyn 环境配置文件的概念。 diff --git a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx index 7311105a9..cf9cb8fc5 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/websockets.mdx @@ -176,6 +176,45 @@ export const description = --- +## 便捷查询参数访问 {{ tag: 'easy_access', label: 'easy_access' }} + + + + 除了手动调用 `websocket.query_params.get(...)` 之外,你还可以在处理函数、`on_connect` 和 `on_close` 的签名中直接声明带类型注解的查询参数。Robyn 会自动解析并进行类型转换——与 HTTP 便捷参数访问的用法一致。 + + 带默认值的参数是可选的。没有默认值的参数是必需的——如果缺少,连接将被拒绝并返回错误消息。 + + + + + ```python {{ title: '处理函数' }} + @app.websocket("/ws") + async def handler(websocket, room: str = "default", page: int = 1): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text( + f"room={room} page={page} msg={msg}" + ) + except WebSocketDisconnect: + pass + ``` + + ```python {{ title: '回调' }} + @handler.on_connect + def on_connect(websocket, room: str = "default"): + return f"已连接到 {room}" + + @handler.on_close + def on_close(websocket, room: str = "default"): + return f"已离开 {room}" + ``` + + + + +--- + ## 关闭连接 {{ tag: 'close', label: 'close' }} diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 788dd0593..13a20e685 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -4,7 +4,7 @@ import pathlib import time from collections import defaultdict -from typing import Optional +from typing import List, Optional from integration_tests.subroutes import di_subrouter, static_router, sub_router from robyn import Headers, Request, Response, Robyn, SSEMessage, SSEResponse, WebSocketDisconnect, jsonify, serve_file, serve_html @@ -1430,6 +1430,90 @@ def event_generator(): return SSEResponse(event_generator(), status_code=201) +# ===== Easy Access Query/Path Parameters ===== + + +@app.get("/easy/sync/:id") +def easy_access_sync(id: int, q: str, page: int = 1): + return {"id": id, "q": q, "page": page} + + +@app.get("/easy/async/:id") +async def easy_access_async(id: int, q: str, page: int = 1): + return {"id": id, "q": q, "page": page} + + +@app.get("/easy/sync/optional") +def easy_access_optional_sync(name: str, age: Optional[int] = None): + return {"name": name, "age": age} + + +@app.get("/easy/async/optional") +async def easy_access_optional_async(name: str, age: Optional[int] = None): + return {"name": name, "age": age} + + +@app.get("/easy/sync/list") +def easy_access_list_sync(tag: List[str]): + return {"tags": tag} + + +@app.get("/easy/async/list") +async def easy_access_list_async(tag: List[str]): + return {"tags": tag} + + +@app.get("/easy/sync/bool") +def easy_access_bool_sync(active: bool = False): + return {"active": active} + + +@app.get("/easy/async/bool") +async def easy_access_bool_async(active: bool = False): + return {"active": active} + + +@app.get("/easy/sync/mixed/:id") +def easy_access_mixed_sync(request: Request, id: int, q: str = ""): + return {"id": id, "q": q, "method": request.method} + + +@app.get("/easy/async/mixed/:id") +async def easy_access_mixed_async(request: Request, id: int, q: str = ""): + return {"id": id, "q": q, "method": request.method} + + +@app.get("/easy/sync/float") +def easy_access_float_sync(price: float): + return {"price": price} + + +@app.get("/easy/async/float") +async def easy_access_float_async(price: float): + return {"price": price} + + +# --- WebSocket with easy access query params --- +@app.websocket("/web_socket_easy_access") +async def easy_access_ws_handler(websocket, room: str = "default", page: int = 1): + try: + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"room={room} page={page} msg={msg}") + except WebSocketDisconnect: + pass + + +@easy_access_ws_handler.on_connect +def easy_access_ws_on_connect(websocket, room: str = "default"): + return f"connected to {room}" + + +@easy_access_ws_handler.on_close +def easy_access_ws_on_close(websocket, room: str = "default"): + return f"left {room}" + + def main(): app.set_response_header("server", "robyn") app.serve_directory( diff --git a/integration_tests/test_easy_access_params.py b/integration_tests/test_easy_access_params.py new file mode 100644 index 000000000..ea359144e --- /dev/null +++ b/integration_tests/test_easy_access_params.py @@ -0,0 +1,152 @@ +import os + +import pytest +from websocket import create_connection + +from integration_tests.helpers.http_methods_helpers import get + +WS_BASE_URL = f"ws://127.0.0.1:{os.environ.get('ROBYN_PORT', '8080')}" + + +# ===== HTTP: Path param + query param with type coercion ===== + + +@pytest.mark.benchmark +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_path_and_query_params(session, function_type): + r = get(f"/easy/{function_type}/42?q=hello&page=5") + assert r.json() == {"id": 42, "q": "hello", "page": 5} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_default_value(session, function_type): + r = get(f"/easy/{function_type}/42?q=hello") + assert r.json() == {"id": 42, "q": "hello", "page": 1} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_missing_required_param(session, function_type): + """Missing required 'q' param should return 400.""" + r = get(f"/easy/{function_type}/42", should_check_response=False) + assert r.status_code == 400 + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_bad_type_coercion(session, function_type): + """Path param :id declared as int but given 'abc' should return 400.""" + r = get(f"/easy/{function_type}/abc?q=hello", should_check_response=False) + assert r.status_code == 400 + + +# ===== HTTP: Optional params ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_optional_present(session, function_type): + r = get(f"/easy/{function_type}/optional?name=bob&age=30") + assert r.json() == {"name": "bob", "age": 30} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_optional_missing(session, function_type): + r = get(f"/easy/{function_type}/optional?name=bob") + assert r.json() == {"name": "bob", "age": None} + + +# ===== HTTP: List params ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_list_params(session, function_type): + r = get(f"/easy/{function_type}/list?tag=python&tag=rust&tag=web") + assert r.json() == {"tags": ["python", "rust", "web"]} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_list_single_value(session, function_type): + r = get(f"/easy/{function_type}/list?tag=python") + assert r.json() == {"tags": ["python"]} + + +# ===== HTTP: Bool params ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_bool_true(session, function_type): + r = get(f"/easy/{function_type}/bool?active=true") + assert r.json() == {"active": True} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_bool_false(session, function_type): + r = get(f"/easy/{function_type}/bool?active=false") + assert r.json() == {"active": False} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_bool_default(session, function_type): + r = get(f"/easy/{function_type}/bool") + assert r.json() == {"active": False} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_bool_numeric(session, function_type): + r = get(f"/easy/{function_type}/bool?active=1") + assert r.json() == {"active": True} + + +# ===== HTTP: Mixed (Request object + individual params) ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_mixed_with_request(session, function_type): + r = get(f"/easy/{function_type}/mixed/99?q=search") + assert r.json() == {"id": 99, "q": "search", "method": "GET"} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_mixed_with_default(session, function_type): + r = get(f"/easy/{function_type}/mixed/99") + assert r.json() == {"id": 99, "q": "", "method": "GET"} + + +# ===== HTTP: Float params ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_float(session, function_type): + r = get(f"/easy/{function_type}/float?price=19.99") + assert r.json() == {"price": 19.99} + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_easy_access_float_bad_value(session, function_type): + r = get(f"/easy/{function_type}/float?price=notanumber", should_check_response=False) + assert r.status_code == 400 + + +# ===== WebSocket: Easy access query params ===== + + +def test_easy_access_ws_with_params(session): + ws = create_connection(f"{WS_BASE_URL}/web_socket_easy_access?room=chat&page=5") + connect_msg = ws.recv() + assert connect_msg == "connected to chat" + + ws.send("hello") + response = ws.recv() + assert response == "room=chat page=5 msg=hello" + + ws.close() + + +def test_easy_access_ws_with_defaults(session): + ws = create_connection(f"{WS_BASE_URL}/web_socket_easy_access") + connect_msg = ws.recv() + assert connect_msg == "connected to default" + + ws.send("hello") + response = ws.recv() + assert response == "room=default page=1 msg=hello" + + ws.close() diff --git a/integration_tests/test_split_request_params.py b/integration_tests/test_split_request_params.py index 37afb10b3..0b4cbacd0 100644 --- a/integration_tests/test_split_request_params.py +++ b/integration_tests/test_split_request_params.py @@ -81,8 +81,10 @@ def test_split_request_params_typed_untyped_post_combined(session, function_type @pytest.mark.benchmark @pytest.mark.parametrize("function_type", ["sync", "async"]) def test_split_request_params_get_combined_failure(session, function_type): + # 'vishnu' is an unknown param with no default — now returns 400 (was 500 before easy-access params) + # because unresolved params are treated as missing required query params res = post(f"/{function_type}/split_request_typed_untyped/combined/failure?hello=robyn&a=1&b=2", data={"hello": "world"}, should_check_response=False) - assert 500 == res.status_code + assert 400 == res.status_code @pytest.mark.benchmark diff --git a/llms.txt b/llms.txt index 8e9414a1a..82ddc93b8 100644 --- a/llms.txt +++ b/llms.txt @@ -48,6 +48,7 @@ app.start(port=8080) - **Authentication**: AuthenticationHandler base class for custom auth - **Static Files**: Directory serving via `app.serve_directory()` - **SSE**: Server-Sent Events support via `SSEResponse` +- **Easy Access Parameters**: Typed path/query params with automatic coercion in handler signatures - **Direct Rust Integration**: Embed Rust code directly in routes ## Project Structure @@ -161,6 +162,34 @@ def on_close(websocket): return "Closed" ``` +### Easy Access Parameters +Declare typed path and query parameters directly in handler signatures. Works for both HTTP and WebSocket handlers. + +```python +from typing import List, Optional + +# HTTP: path params + query params with type coercion +@app.get("/items/:id") +async def get_item(id: int, q: str, page: int = 1): + return {"id": id, "q": q, "page": page} + +# Optional, List, and bool params +@app.get("/search") +def search(name: str, tags: List[str], active: bool = False, age: Optional[int] = None): + return {"name": name, "tags": tags, "active": active, "age": age} + +# WebSocket: typed query params on handler and callbacks +@app.websocket("/ws") +async def handler(websocket, room: str = "default", page: int = 1): + while True: + msg = await websocket.receive_text() + await websocket.send_text(f"room={room} page={page} msg={msg}") + +@handler.on_connect +def on_connect(websocket, room: str = "default"): + return f"connected to {room}" +``` + ### MCP (Model Context Protocol) ```python @app.mcp.resource("time://current") diff --git a/robyn/_param_utils.py b/robyn/_param_utils.py new file mode 100644 index 000000000..e0789bf5f --- /dev/null +++ b/robyn/_param_utils.py @@ -0,0 +1,191 @@ +""" +Shared utilities for resolving individual query/path parameters +from handler function signatures, with type coercion. + +Used by both robyn/router.py (HTTP handlers) and robyn/ws.py (WebSocket handlers). +""" + +import inspect +import logging +from typing import Any, Dict, Optional, Set, Tuple, Union + +_logger = logging.getLogger(__name__) + +_MISSING = object() + +# Values that map to True/False from query string bool params +_BOOL_TRUE_STRINGS = frozenset({"true", "1", "yes", "on"}) +_BOOL_FALSE_STRINGS = frozenset({"false", "0", "no", "off", ""}) + + +class QueryParamValidationError(Exception): + """Raised when a query or path parameter cannot be coerced to the expected type, + or when a required parameter is missing.""" + + def __init__(self, param_name: str, value: Optional[str], expected_type: type, message: Optional[str] = None): + self.param_name = param_name + self.value = value + self.expected_type = expected_type + if message: + self.detail = message + elif value is None: + self.detail = f"Missing required parameter: '{param_name}'" + else: + self.detail = f"Invalid value '{value}' for parameter '{param_name}': expected {expected_type.__name__}" + super().__init__(self.detail) + + +def unwrap_optional(annotation) -> Tuple[Any, bool]: + """ + If annotation is Optional[T] (i.e. Union[T, None]), return (T, True). + Otherwise return (annotation, False). + """ + origin = getattr(annotation, "__origin__", None) + if origin is Union: + args = annotation.__args__ + non_none_args = [a for a in args if a is not type(None)] + if len(non_none_args) == 1 and type(None) in args: + return non_none_args[0], True + return annotation, False + + +def is_list_type(annotation) -> bool: + """Check if annotation is List[T] or list[T].""" + origin = getattr(annotation, "__origin__", None) + return origin is list + + +def get_list_element_type(annotation) -> type: + """Get the element type from List[T]. Defaults to str if not specified.""" + args = getattr(annotation, "__args__", None) + if args and len(args) > 0: + return args[0] + return str + + +def coerce_value(value: str, target_type: type, param_name: str): + """ + Convert a string value to the target type. + Raises QueryParamValidationError on failure. + """ + if target_type is str or target_type is inspect.Parameter.empty: + return value + + try: + if target_type is int: + return int(value) + if target_type is float: + return float(value) + if target_type is bool: + lower = value.lower() + if lower in _BOOL_TRUE_STRINGS: + return True + if lower in _BOOL_FALSE_STRINGS: + return False + raise ValueError(f"Cannot interpret '{value}' as bool") + # Fallback: try calling the type constructor (covers Enum, UUID, etc.) + return target_type(value) + except (ValueError, TypeError) as e: + raise QueryParamValidationError(param_name, value, target_type) from e + + +def resolve_individual_params( + unresolved_params: Dict[str, inspect.Parameter], + query_params, + path_params: Optional[Dict[str, str]], + route_param_names: Set[str], +) -> Dict[str, Any]: + """ + Resolve handler parameters as individual path or query parameters. + + For each unresolved parameter: + 1. If its name matches a route param name (from the endpoint pattern), look it up in path_params. + 2. Otherwise, look it up in query_params. + 3. Apply type coercion based on the parameter's annotation. + 4. Fall back to the parameter's default value, or None for Optional types. + 5. Raise QueryParamValidationError if a required parameter is missing. + + Args: + unresolved_params: dict of param_name -> inspect.Parameter for params not yet resolved. + query_params: QueryParams object with .get() and .get_all() methods. + path_params: dict of path parameter values (may be None for WebSocket). + route_param_names: set of parameter names declared in the route pattern (e.g. from /:id). + + Returns: + dict mapping param names to their resolved values. + """ + resolved = {} + + for param_name, param in unresolved_params.items(): + annotation = param.annotation + if annotation is inspect.Parameter.empty: + annotation = str + + inner_type, is_optional = unwrap_optional(annotation) + is_list = is_list_type(inner_type) + elem_type = get_list_element_type(inner_type) if is_list else inner_type + + raw_value = _MISSING + + # 1. Check path params first + if path_params is not None and param_name in route_param_names: + pv = path_params.get(param_name) + if pv is not None: + raw_value = pv + + # 2. Check query params + if raw_value is _MISSING and query_params is not None: + if is_list: + all_values = query_params.get_all(param_name) + if all_values is not None: + resolved[param_name] = [coerce_value(v, elem_type, param_name) for v in all_values] + continue + else: + qp_value = query_params.get(param_name, None) + if qp_value is not None: + raw_value = qp_value + + # 3. Got a value — coerce it + if raw_value is not _MISSING: + resolved[param_name] = coerce_value(raw_value, inner_type, param_name) + continue + + # 4. Use default value if available + if param.default is not inspect.Parameter.empty: + resolved[param_name] = param.default + continue + + # 5. Optional with no default -> None + if is_optional: + resolved[param_name] = None + continue + + # 6. Truly missing required parameter + raise QueryParamValidationError(param_name, None, elem_type) + + return resolved + + +def parse_route_param_names(endpoint: str) -> Set[str]: + """ + Extract parameter names from a route endpoint pattern. + e.g. "/users/:id/posts/:post_id" -> {"id", "post_id"} + + Walks the string character by character looking for ':' followed by + word characters (alphanumeric + underscore). + """ + names = set() + i = 0 + length = len(endpoint) + while i < length: + if endpoint[i] == ":": + # Start of a param name — collect word characters + i += 1 + start = i + while i < length and (endpoint[i].isalnum() or endpoint[i] == "_"): + i += 1 + if i > start: + names.add(endpoint[start:i]) + else: + i += 1 + return names diff --git a/robyn/router.py b/robyn/router.py index a987039cf..609a3a959 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -6,6 +6,7 @@ from typing import Callable, Dict, List, NamedTuple, Optional, Union from robyn import status_codes +from robyn._param_utils import QueryParamValidationError, parse_route_param_names, resolve_individual_params from robyn.authentication import AuthenticationHandler, AuthenticationNotConfiguredError from robyn.dependency_injection import DependencyMap from robyn.jsonify import jsonify @@ -123,6 +124,20 @@ def add_route( # type: ignore exception_handler: Optional[Callable], injected_dependencies: dict, ) -> Union[Callable, CoroutineType]: + # Pre-compute at registration time + route_param_names = parse_route_param_names(endpoint) + + # Warn if the route declares :param names the handler doesn't use + handler_param_names = set(inspect.signature(handler).parameters.keys()) + unused_route_params = route_param_names - handler_param_names + if unused_route_params: + _logger.warning( + "Route '%s' declares path params %s but handler '%s' doesn't use them", + endpoint, + unused_route_params, + handler.__name__, + ) + def wrapped_handler(*args, **kwargs): # In the execute functions the request is passed into *args request = next(filter(lambda it: isinstance(it, Request), args), None) @@ -146,6 +161,7 @@ def wrapped_handler(*args, **kwargs): "identity": Identity, } + # Phase 1: Type-annotated request components type_filtered_params = {} for handler_param in iter(handler_params): @@ -171,6 +187,7 @@ def wrapped_handler(*args, **kwargs): elif issubclass(handler_param_type, QueryParams): type_filtered_params[handler_param_name] = getattr(request, "query_params") + # Phase 2: Reserved-name request components request_components = { "r": request, "req": request, @@ -194,9 +211,17 @@ def wrapped_handler(*args, **kwargs): filtered_params = dict(**type_filtered_params, **name_filtered_params) - if len(filtered_params) != len(handler_params): - invalid_args = set(handler_params) - set(filtered_params) - raise SyntaxError(f"Unexpected request params found: {invalid_args}") + # Phase 3: Individual path/query param resolution + unresolved_names = set(handler_params) - set(filtered_params) + if unresolved_names: + unresolved = {name: handler_params[name] for name in unresolved_names} + individual_params = resolve_individual_params( + unresolved, + request.query_params, + request.path_params, + route_param_names, + ) + filtered_params.update(individual_params) return handler(**filtered_params) @@ -206,6 +231,12 @@ async def async_inner_handler(*args, **kwargs): response = self._format_response( await wrapped_handler(*args, **kwargs), ) + except QueryParamValidationError as err: + response = Response( + status_code=status_codes.HTTP_400_BAD_REQUEST, + headers=Headers({"Content-Type": "text/plain"}), + description=str(err), + ) except Exception as err: if exception_handler is None: raise @@ -220,6 +251,12 @@ def inner_handler(*args, **kwargs): response = self._format_response( wrapped_handler(*args, **kwargs), ) + except QueryParamValidationError as err: + response = Response( + status_code=status_codes.HTTP_400_BAD_REQUEST, + headers=Headers({"Content-Type": "text/plain"}), + description=str(err), + ) except Exception as err: if exception_handler is None: raise diff --git a/robyn/types.py b/robyn/types.py index 38df4ed30..ac4740734 100644 --- a/robyn/types.py +++ b/robyn/types.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from typing import Dict, NewType, Optional, TypedDict +from robyn._param_utils import QueryParamValidationError + @dataclass class Directory: @@ -76,4 +78,4 @@ def create_user(request: Request, data: MyBody): pass -__all__ = ["JSONResponse", "Body", "JsonBody"] +__all__ = ["JSONResponse", "Body", "JsonBody", "QueryParamValidationError"] diff --git a/robyn/ws.py b/robyn/ws.py index be2d7b5f7..44f5bd3b2 100644 --- a/robyn/ws.py +++ b/robyn/ws.py @@ -6,7 +6,8 @@ import orjson -from robyn.robyn import FunctionInfo, WebSocketConnector +from robyn._param_utils import QueryParamValidationError, resolve_individual_params +from robyn.robyn import FunctionInfo, QueryParams, WebSocketConnector _logger = logging.getLogger(__name__) @@ -119,16 +120,52 @@ def decorator(handler): _on_connect_fn = None _on_close_fn = None - def _get_di_kwargs(func): - """Build DI kwargs for a function based on its signature.""" - sig_params = dict(inspect.signature(func).parameters) + def _resolve_ws_params(func_params, adapter): + """ + Resolve all handler params beyond the first positional (websocket). + + Handles: + - global_dependencies / router_dependencies (DI) + - query_params (whole QueryParams object, by name or type annotation) + - individual query params (everything else, with type coercion) + """ injected = app_instance.dependencies.get_dependency_map(app_instance) - kwargs = {} - if "global_dependencies" in sig_params: - kwargs["global_dependencies"] = injected.get("global_dependencies", {}) - if "router_dependencies" in sig_params: - kwargs["router_dependencies"] = injected.get("router_dependencies", {}) - return kwargs + resolved = {} + unresolved = {} + + for idx, (param_name, param) in enumerate(func_params.items()): + # Skip the websocket adapter (first positional arg, passed separately) + if idx == 0 or param.annotation is WebSocketAdapter: + continue + + # DI: global_dependencies + if param_name == "global_dependencies": + resolved[param_name] = injected.get("global_dependencies", {}) + continue + + # DI: router_dependencies + if param_name == "router_dependencies": + resolved[param_name] = injected.get("router_dependencies", {}) + continue + + # Whole QueryParams object (by type annotation or reserved name) + if param.annotation is QueryParams or param_name == "query_params": + resolved[param_name] = adapter.query_params + continue + + # Everything else: individual query param + unresolved[param_name] = param + + if unresolved: + individual = resolve_individual_params( + unresolved, + adapter.query_params, + path_params=None, # WebSocket has no path params yet + route_param_names=set(), + ) + resolved.update(individual) + + return resolved # --- Connect handler (called by Rust on connection open) --- async def connect_handler(ws): @@ -141,13 +178,18 @@ async def connect_handler(ws): # Create the adapter with the Rust channel adapter = WebSocketAdapter(ws, channel) - # Build DI kwargs for the main handler - di_kwargs = _get_di_kwargs(handler) + # Build resolved kwargs for the main handler + try: + handler_params = inspect.signature(handler).parameters + handler_kwargs = _resolve_ws_params(handler_params, adapter) + except QueryParamValidationError as e: + _logger.warning("WebSocket connection rejected for %s: %s", endpoint, e.detail) + return f"Error: {e.detail}" # Start the user's handler as a long-running asyncio task async def _run_handler(): try: - await handler(adapter, **di_kwargs) + await handler(adapter, **handler_kwargs) except WebSocketDisconnect: pass except ConnectionError: @@ -163,11 +205,19 @@ async def _run_handler(): # Call user's on_connect if defined if _on_connect_fn is not None: connect_adapter = WebSocketAdapter(ws, channel) - connect_di = _get_di_kwargs(_on_connect_fn) + try: + connect_params = inspect.signature(_on_connect_fn).parameters + connect_kwargs = _resolve_ws_params(connect_params, connect_adapter) + except QueryParamValidationError as e: + _logger.warning("WebSocket on_connect rejected for %s: %s", endpoint, e.detail) + task = _connection_tasks.pop(conn_id, None) + if task is not None and not task.done(): + task.cancel() + return f"Error: {e.detail}" if asyncio.iscoroutinefunction(_on_connect_fn): - result = await _on_connect_fn(connect_adapter, **connect_di) + result = await _on_connect_fn(connect_adapter, **connect_kwargs) else: - result = _on_connect_fn(connect_adapter, **connect_di) + result = _on_connect_fn(connect_adapter, **connect_kwargs) return result return None @@ -201,11 +251,16 @@ async def close_handler(ws): # Call user's on_close if defined if _on_close_fn is not None: close_adapter = WebSocketAdapter(ws, None) - close_di = _get_di_kwargs(_on_close_fn) + try: + close_params = inspect.signature(_on_close_fn).parameters + close_kwargs = _resolve_ws_params(close_params, close_adapter) + except QueryParamValidationError as e: + _logger.warning("WebSocket on_close param error for %s: %s", endpoint, e.detail) + return None if asyncio.iscoroutinefunction(_on_close_fn): - result = await _on_close_fn(close_adapter, **close_di) + result = await _on_close_fn(close_adapter, **close_kwargs) else: - result = _on_close_fn(close_adapter, **close_di) + result = _on_close_fn(close_adapter, **close_kwargs) return result return None From 7aeaef46f6affc9a66b9727ad1bc1e975f01754a Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Thu, 5 Mar 2026 20:55:54 +0000 Subject: [PATCH 044/106] Release 0.80.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86bbd14c4..a52d35159 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.79.0" +version = "0.80.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 16d8bb604..65a01e3e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.79.0" +version = "0.80.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index 248ac85e7..0fea03b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.79.0" +version = "0.80.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -67,7 +67,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.79.0" +version = "0.80.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 36a7b0d3f3b634ceebf518236ad174c4c4db74d5 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:05:32 +0000 Subject: [PATCH 045/106] feat: integrate pydantic (#1325) * feat: integrate pydantic * feat: integrate pydantic * update docs * update * integrate mode pydantic * update * complete the integration * docs update * update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 10 +- .../components/documentation/Navigation.jsx | 6 + .../en/api_reference/openapi.mdx | 27 +- .../en/api_reference/pydantic.mdx | 366 ++++++++++++++ .../zh/api_reference/openapi.mdx | 26 + .../zh/api_reference/pydantic.mdx | 366 ++++++++++++++ integration_tests/base_routes.py | 135 ++++- integration_tests/test_openapi.py | 246 +++++++++- integration_tests/test_pydantic.py | 461 ++++++++++++++++++ pyproject.toml | 5 + robyn/openapi.py | 75 ++- robyn/pydantic_support.py | 225 +++++++++ robyn/router.py | 75 ++- robyn/types.py | 2 +- 14 files changed, 1991 insertions(+), 34 deletions(-) create mode 100644 docs_src/src/pages/documentation/en/api_reference/pydantic.mdx create mode 100644 docs_src/src/pages/documentation/zh/api_reference/pydantic.mdx create mode 100644 integration_tests/test_pydantic.py create mode 100644 robyn/pydantic_support.py diff --git a/README.md b/README.md index 230c4b2d7..a116ed7a6 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,22 @@ Source: [TechEmpower Round 22](https://www.techempower.com/benchmarks/#section=d You can simply use Pip for installation. -``` +```bash pip install robyn ``` Or, with [conda-forge](https://conda-forge.org/) -``` +```bash conda install -c conda-forge robyn ``` +To install with all optional features (Pydantic validation, Jinja2 templating): + +```bash +pip install "robyn[all]" +``` + ## 🤔 Usage ### 🚀 Define your API diff --git a/docs_src/src/components/documentation/Navigation.jsx b/docs_src/src/components/documentation/Navigation.jsx index ad26b5c79..fc9ec65d7 100644 --- a/docs_src/src/components/documentation/Navigation.jsx +++ b/docs_src/src/components/documentation/Navigation.jsx @@ -320,6 +320,10 @@ export const navigation = [ href: '/documentation/en/api_reference/openapi', title: 'OpenAPI Documentation', }, + { + href: '/documentation/en/api_reference/pydantic', + title: 'Pydantic Integration', + }, { href: '/documentation/en/api_reference/dependency_injection', title: 'Dependency Injection', @@ -467,6 +471,7 @@ const translations = { 'Direct Rust Usage': 'Direct Rust Usage', 'GraphQL Support': 'GraphQL Support', 'Dependency Injection': 'Dependency Injection', + 'Pydantic Integration': 'Pydantic Integration', 'AI': 'AI', 'AI Agents': 'AI Agents', Talks: 'Talks', @@ -513,6 +518,7 @@ const translations = { 'Direct Rust Usage': '直接使用 Rust', 'GraphQL Support': 'GraphQL 支持', 'Dependency Injection': '依赖注入', + 'Pydantic Integration': 'Pydantic 集成', 'AI': 'AI', 'AI Agents': 'AI 代理', 'Talks': '演讲', diff --git a/docs_src/src/pages/documentation/en/api_reference/openapi.mdx b/docs_src/src/pages/documentation/en/api_reference/openapi.mdx index d82e3e935..67b978b1d 100644 --- a/docs_src/src/pages/documentation/en/api_reference/openapi.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/openapi.mdx @@ -168,6 +168,31 @@ def create_item(request: Request, body: CreateItemBody) -> CreateResponse: With the reference documentation deployed and running smoothly, Batman had a powerful new tool at his disposal. The Robyn framework had provided him with the flexibility, scalability, and performance needed to create an effective crime-fighting application, giving him a technological edge in his ongoing battle to protect Gotham City. +## Using Pydantic Models + +If you have Pydantic installed (`pip install "robyn[pydantic]"` or `pip install "robyn[all]"`), you can use Pydantic `BaseModel` classes directly as handler parameter annotations. Robyn will automatically validate the request body **and** generate a rich OpenAPI schema — including property types, required fields, defaults, and `$ref` for nested models. + + + +```python +from pydantic import BaseModel + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + +@app.post("/users", openapi_tags=["Users"]) +def create_user(user: UserCreate) -> dict: + """Create a new user""" + return {"name": user.name} +``` + + + +For the full guide on Pydantic validation, nested models, error responses, and OpenAPI integration, see the dedicated [Pydantic Integration](/documentation/en/api_reference/pydantic) page. + ## What's next? @@ -176,7 +201,7 @@ Batman wondered about whether Robyn handlers can be dispatched to multiple proce Robyn showed him the way! -[Multitiprocess Execution](/documentation/en/api_reference/multiprocess_execution) +[Multiprocess Execution](/documentation/en/api_reference/multiprocess_execution) diff --git a/docs_src/src/pages/documentation/en/api_reference/pydantic.mdx b/docs_src/src/pages/documentation/en/api_reference/pydantic.mdx new file mode 100644 index 000000000..73882fa58 --- /dev/null +++ b/docs_src/src/pages/documentation/en/api_reference/pydantic.mdx @@ -0,0 +1,366 @@ +export const description = + 'Learn how to use Pydantic models with Robyn for automatic request body validation and OpenAPI schema generation.' + + +## Pydantic Integration + +Robyn supports [Pydantic](https://docs.pydantic.dev/) v2 as an optional dependency for automatic request body validation and rich OpenAPI schema generation. Validation is **opt-in per handler** — it only activates when you annotate a parameter with a Pydantic `BaseModel`. Handlers without Pydantic annotations are completely unaffected: no parsing, no validation, no overhead. When Pydantic is not installed at all, Robyn never imports it. + + +## Installation + +Install Robyn with Pydantic support using the optional extra: + + + +```bash {{ title: 'Pydantic only' }} +pip install "robyn[pydantic]" +``` + +```bash {{ title: 'All extras' }} +pip install "robyn[all]" +``` + +```bash {{ title: 'conda' }} +conda install robyn pydantic -c conda-forge +``` + + + +`robyn[all]` includes Pydantic, Jinja2 templating, and any future optional features. + + +## Basic Usage + +Define a Pydantic `BaseModel` and use it as a type annotation on your handler parameter. Robyn will automatically parse the incoming JSON body, validate it against the model, and inject the validated instance into your handler. + + + +```python {{ title: 'Synchronous' }} +from pydantic import BaseModel +from robyn import Robyn + +app = Robyn(__file__) + + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + + +@app.post("/users") +def create_user(user: UserCreate): + """Create a new user""" + return { + "name": user.name, + "email": user.email, + "age": user.age, + "active": user.active, + } + + +if __name__ == "__main__": + app.start() +``` + +```python {{ title: 'Asynchronous' }} +from pydantic import BaseModel +from robyn import Robyn + +app = Robyn(__file__) + + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + + +@app.post("/users") +async def create_user(user: UserCreate): + """Create a new user""" + return { + "name": user.name, + "email": user.email, + "age": user.age, + "active": user.active, + } + + +if __name__ == "__main__": + app.start() +``` + + + + +## Validation Errors + +When the request body fails validation, Robyn automatically returns a **422 Unprocessable Entity** response with structured error details. You do not need to write any error handling code. + +For example, sending `{"name": "Alice", "email": "alice@example.com", "age": "not_a_number"}` would produce: + +```json +{ + "error": "Validation Error", + "detail": [ + { + "type": "int_parsing", + "loc": ["age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "not_a_number" + } + ] +} +``` + +Missing required fields are also caught: + +```json +{ + "error": "Validation Error", + "detail": [ + { + "type": "missing", + "loc": ["email"], + "msg": "Field required", + "input": {"name": "Alice", "age": 30} + } + ] +} +``` + + +## Nested Models + +Pydantic models can reference other models. Robyn handles nested validation automatically. + + + +```python +from pydantic import BaseModel +from robyn import Robyn + +app = Robyn(__file__) + + +class Address(BaseModel): + street: str + city: str + zip_code: str + + +class UserWithAddress(BaseModel): + name: str + email: str + address: Address + + +@app.post("/users") +def create_user(data: UserWithAddress): + """Create a user with an address""" + return {"name": data.name, "city": data.address.city} +``` + + + +If the nested `address` object is missing or malformed, Robyn returns a 422 with the full error path (e.g. `["address", "city"]`). + + +## Using with the Request Object + +You can combine Pydantic parameters with the standard `Request` object in the same handler. This gives you access to headers, query params, and other request metadata alongside the validated body. + + + +```python +from pydantic import BaseModel +from robyn import Robyn, Request + +app = Robyn(__file__) + + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + + +@app.post("/users") +def create_user(request: Request, user: UserCreate): + """Create a user — access both raw request and validated model""" + return { + "method": request.method, + "name": user.name, + "email": user.email, + } +``` + + + + +## Returning Pydantic Models Directly + +You can return a Pydantic model instance (or a list of them) directly from a handler. Robyn will automatically serialize it to JSON with the correct `Content-Type` header — no need to call `.model_dump()` manually. + + + +```python {{ title: 'Single model' }} +@app.post("/users") +def create_user(user: UserCreate) -> UserCreate: + """Validate and echo back the user""" + return user +``` + +```python {{ title: 'List of models' }} +@app.post("/users/batch") +def create_users(user: UserCreate) -> list[UserCreate]: + """Return multiple model instances""" + return [user, user] +``` + + + +Both forms produce an `application/json` response. The single-model path uses Pydantic's Rust-based `model_dump_json()` for maximum throughput. + + +## How Validation Is Triggered + +Pydantic validation is **annotation-driven, not method-driven**. The router inspects each handler's signature at registration time; any parameter annotated with a `BaseModel` subclass triggers automatic validation of `request.body` when that route is called. This works with every HTTP method — `POST`, `PUT`, `PATCH`, `DELETE`, or any other method that carries a body. + + + +```python {{ title: 'PUT' }} +@app.put("/users/:id") +def update_user(user: UserCreate): + return {"updated": True, "name": user.name} +``` + +```python {{ title: 'PATCH' }} +@app.patch("/users/:id") +def patch_user(user: UserCreate): + return {"patched": True, "name": user.name} +``` + + + + +## OpenAPI Integration + +When you use Pydantic models, Robyn automatically generates rich JSON Schema in your OpenAPI specification at `/openapi.json`. This includes: + +- **Property types** — `string`, `integer`, `boolean`, etc. +- **Required fields** — fields without defaults are listed in `required` +- **Default values** — shown in the schema +- **Nested models** — referenced via `$ref` and placed in `components/schemas` + + + +```python +from pydantic import BaseModel +from robyn import Robyn, Request + +app = Robyn(__file__) + + +class Address(BaseModel): + street: str + city: str + zip_code: str + + +class UserWithAddress(BaseModel): + name: str + email: str + address: Address + + +@app.post("/users", openapi_tags=["Users"]) +def create_user(request: Request, data: UserWithAddress) -> dict: + """Create a user with a nested address""" + return {"name": data.name, "city": data.address.city} +``` + + + +The generated `/openapi.json` will contain: + +```json +{ + "paths": { + "/users": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": {"type": "string", "title": "Name"}, + "email": {"type": "string", "title": "Email"}, + "address": {"$ref": "#/components/schemas/Address"} + }, + "required": ["name", "email", "address"], + "title": "UserWithAddress" + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "type": "object", + "properties": { + "street": {"type": "string", "title": "Street"}, + "city": {"type": "string", "title": "City"}, + "zip_code": {"type": "string", "title": "Zip Code"} + }, + "required": ["street", "city", "zip_code"], + "title": "Address" + } + } + } +} +``` + + +## Pydantic vs Body + +Robyn supports two approaches for typed request bodies. Choose the one that fits your needs: + +| Feature | `Body` subclass | Pydantic `BaseModel` | +|---|---|---| +| Installation | Built-in | `pip install "robyn[pydantic]"` | +| Validation | No automatic validation | Full validation with detailed errors | +| Error responses | Manual | Automatic 422 with structured errors | +| Return serialization | Manual `dict()` | Auto-serialize model to JSON | +| OpenAPI schema | Basic type inference | Full JSON Schema (types, required, defaults, `$ref`) | +| Nested models | Supported (basic) | Supported (with `$ref` in OpenAPI) | +| Performance overhead | None | Only when Pydantic is installed and used | + +Both approaches work with OpenAPI documentation. If you need validation, use Pydantic. If you just need OpenAPI schema hints without validation, `Body` is sufficient. + + +## Important Notes + +- **Opt-in per handler** — Validation only runs on handlers where a parameter is annotated with a Pydantic `BaseModel`. All other handlers (using `Body`, `Request`, path params, etc.) behave exactly as before with zero additional overhead. +- **One Pydantic body per handler** — Each handler can have at most one parameter annotated with a Pydantic model. The entire request body is parsed into that single model. If you need multiple model inputs, compose them into a single parent model with nested fields. +- **Request validation only** — Robyn validates *incoming* request bodies against Pydantic models but does not validate *outgoing* responses. When you return a model instance, it is serialized as-is without re-validation. This is a deliberate design choice for performance — if you constructed the model, it's already valid. + + +## What's next? + +Batman wondered about whether Robyn handlers can be dispatched to multiple processes. + +Robyn showed him the way! + +[Multiprocess Execution](/documentation/en/api_reference/multiprocess_execution) + + diff --git a/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx b/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx index 2e564e5e6..072c099c2 100644 --- a/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx +++ b/docs_src/src/pages/documentation/zh/api_reference/openapi.mdx @@ -167,6 +167,32 @@ def create_item(request: Request, body: CreateItemBody) -> CreateResponse: 随着参考文档的成功部署,蝙蝠侠拥有了一个强大的新工具。Robyn 框架为他提供了创建高效打击犯罪应用所需的灵活性、可扩展性和性能,使他在保护哥谭市的持续战斗中获得了技术优势。 +## 使用 Pydantic 模型 + +如果您已安装 Pydantic(`pip install "robyn[pydantic]"` 或 `pip install "robyn[all]"`),可以直接使用 Pydantic `BaseModel` 类作为处理函数参数的类型注解。Robyn 将自动验证请求体,**并且**生成丰富的 OpenAPI Schema — 包括属性类型、必填字段、默认值,以及嵌套模型的 `$ref` 引用。 + + + +```python +from pydantic import BaseModel + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + +@app.post("/users", openapi_tags=["Users"]) +def create_user(user: UserCreate) -> dict: + """创建新用户""" + return {"name": user.name} +``` + + + +有关 Pydantic 验证、嵌套模型、错误响应和 OpenAPI 集成的完整指南,请参阅 [Pydantic 集成](/documentation/zh/api_reference/pydantic) 页面。 + + ## 下一步 完成接口文档配置后,蝙蝠侠开始思考如何提升应用程序的并发处理能力。 diff --git a/docs_src/src/pages/documentation/zh/api_reference/pydantic.mdx b/docs_src/src/pages/documentation/zh/api_reference/pydantic.mdx new file mode 100644 index 000000000..32e270036 --- /dev/null +++ b/docs_src/src/pages/documentation/zh/api_reference/pydantic.mdx @@ -0,0 +1,366 @@ +export const description = + '了解如何在 Robyn 中使用 Pydantic 模型实现自动请求体验证和 OpenAPI 文档生成。' + + +## Pydantic 集成 + +Robyn 支持 [Pydantic](https://docs.pydantic.dev/) v2 作为可选依赖,用于自动请求体验证和丰富的 OpenAPI 文档生成。验证是**按处理函数选择启用**的 — 只有当您使用 Pydantic `BaseModel` 注解参数时才会激活。未使用 Pydantic 注解的处理函数完全不受影响:不进行解析、不进行验证、无额外开销。当未安装 Pydantic 时,Robyn 不会导入它。 + + +## 安装 + +使用可选扩展安装带 Pydantic 支持的 Robyn: + + + +```bash {{ title: '仅 Pydantic' }} +pip install "robyn[pydantic]" +``` + +```bash {{ title: '所有扩展' }} +pip install "robyn[all]" +``` + +```bash {{ title: 'conda' }} +conda install robyn pydantic -c conda-forge +``` + + + +`robyn[all]` 包含 Pydantic、Jinja2 模板引擎以及未来的所有可选功能。 + + +## 基本用法 + +定义一个 Pydantic `BaseModel`,并将其用作处理函数参数的类型注解。Robyn 将自动解析传入的 JSON 请求体,根据模型进行验证,并将验证后的实例注入到处理函数中。 + + + +```python {{ title: '同步' }} +from pydantic import BaseModel +from robyn import Robyn + +app = Robyn(__file__) + + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + + +@app.post("/users") +def create_user(user: UserCreate): + """创建新用户""" + return { + "name": user.name, + "email": user.email, + "age": user.age, + "active": user.active, + } + + +if __name__ == "__main__": + app.start() +``` + +```python {{ title: '异步' }} +from pydantic import BaseModel +from robyn import Robyn + +app = Robyn(__file__) + + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + + +@app.post("/users") +async def create_user(user: UserCreate): + """创建新用户""" + return { + "name": user.name, + "email": user.email, + "age": user.age, + "active": user.active, + } + + +if __name__ == "__main__": + app.start() +``` + + + + +## 验证错误 + +当请求体验证失败时,Robyn 会自动返回 **422 Unprocessable Entity** 响应,并附带结构化的错误详情。您无需编写任何错误处理代码。 + +例如,发送 `{"name": "Alice", "email": "alice@example.com", "age": "not_a_number"}` 将产生: + +```json +{ + "error": "Validation Error", + "detail": [ + { + "type": "int_parsing", + "loc": ["age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "not_a_number" + } + ] +} +``` + +缺少必填字段也会被捕获: + +```json +{ + "error": "Validation Error", + "detail": [ + { + "type": "missing", + "loc": ["email"], + "msg": "Field required", + "input": {"name": "Alice", "age": 30} + } + ] +} +``` + + +## 嵌套模型 + +Pydantic 模型可以引用其他模型。Robyn 会自动处理嵌套验证。 + + + +```python +from pydantic import BaseModel +from robyn import Robyn + +app = Robyn(__file__) + + +class Address(BaseModel): + street: str + city: str + zip_code: str + + +class UserWithAddress(BaseModel): + name: str + email: str + address: Address + + +@app.post("/users") +def create_user(data: UserWithAddress): + """创建带有地址的用户""" + return {"name": data.name, "city": data.address.city} +``` + + + +如果嵌套的 `address` 对象缺失或格式错误,Robyn 将返回 422 响应,并包含完整的错误路径(例如 `["address", "city"]`)。 + + +## 与 Request 对象配合使用 + +您可以在同一个处理函数中同时使用 Pydantic 参数和标准的 `Request` 对象。这样您可以在获取验证后的请求体的同时,访问请求头、查询参数和其他请求元数据。 + + + +```python +from pydantic import BaseModel +from robyn import Robyn, Request + +app = Robyn(__file__) + + +class UserCreate(BaseModel): + name: str + email: str + age: int + active: bool = True + + +@app.post("/users") +def create_user(request: Request, user: UserCreate): + """创建用户 — 同时访问原始请求和验证后的模型""" + return { + "method": request.method, + "name": user.name, + "email": user.email, + } +``` + + + + +## 直接返回 Pydantic 模型 + +您可以直接从处理函数返回 Pydantic 模型实例(或模型列表)。Robyn 会自动将其序列化为 JSON 并设置正确的 `Content-Type` 头 — 无需手动调用 `.model_dump()`。 + + + +```python {{ title: '单个模型' }} +@app.post("/users") +def create_user(user: UserCreate) -> UserCreate: + """验证并直接返回用户""" + return user +``` + +```python {{ title: '模型列表' }} +@app.post("/users/batch") +def create_users(user: UserCreate) -> list[UserCreate]: + """返回多个模型实例""" + return [user, user] +``` + + + +两种形式都会生成 `application/json` 响应。单个模型路径使用 Pydantic 基于 Rust 的 `model_dump_json()` 以获得最大吞吐量。 + + +## 验证触发机制 + +Pydantic 验证是**基于注解驱动的,而非基于 HTTP 方法**。路由器在注册时检查每个处理函数的签名;任何使用 `BaseModel` 子类注解的参数都会在路由被调用时自动触发 `request.body` 的验证。这适用于所有 HTTP 方法 — `POST`、`PUT`、`PATCH`、`DELETE` 或任何携带请求体的方法。 + + + +```python {{ title: 'PUT' }} +@app.put("/users/:id") +def update_user(user: UserCreate): + return {"updated": True, "name": user.name} +``` + +```python {{ title: 'PATCH' }} +@app.patch("/users/:id") +def patch_user(user: UserCreate): + return {"patched": True, "name": user.name} +``` + + + + +## OpenAPI 集成 + +当您使用 Pydantic 模型时,Robyn 会自动在 `/openapi.json` 的 OpenAPI 规范中生成丰富的 JSON Schema。包括: + +- **属性类型** — `string`、`integer`、`boolean` 等 +- **必填字段** — 没有默认值的字段会列在 `required` 中 +- **默认值** — 在 Schema 中展示 +- **嵌套模型** — 通过 `$ref` 引用,并放置在 `components/schemas` 中 + + + +```python +from pydantic import BaseModel +from robyn import Robyn, Request + +app = Robyn(__file__) + + +class Address(BaseModel): + street: str + city: str + zip_code: str + + +class UserWithAddress(BaseModel): + name: str + email: str + address: Address + + +@app.post("/users", openapi_tags=["Users"]) +def create_user(request: Request, data: UserWithAddress) -> dict: + """创建带有嵌套地址的用户""" + return {"name": data.name, "city": data.address.city} +``` + + + +生成的 `/openapi.json` 将包含: + +```json +{ + "paths": { + "/users": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": {"type": "string", "title": "Name"}, + "email": {"type": "string", "title": "Email"}, + "address": {"$ref": "#/components/schemas/Address"} + }, + "required": ["name", "email", "address"], + "title": "UserWithAddress" + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "type": "object", + "properties": { + "street": {"type": "string", "title": "Street"}, + "city": {"type": "string", "title": "City"}, + "zip_code": {"type": "string", "title": "Zip Code"} + }, + "required": ["street", "city", "zip_code"], + "title": "Address" + } + } + } +} +``` + + +## Pydantic 与 Body 对比 + +Robyn 支持两种类型化请求体的方式。根据您的需求选择合适的方式: + +| 特性 | `Body` 子类 | Pydantic `BaseModel` | +|---|---|---| +| 安装 | 内置 | `pip install "robyn[pydantic]"` | +| 验证 | 无自动验证 | 完整验证,附带详细错误信息 | +| 错误响应 | 手动处理 | 自动返回 422 结构化错误 | +| 返回序列化 | 手动 `dict()` | 自动序列化模型为 JSON | +| OpenAPI Schema | 基本类型推断 | 完整 JSON Schema(类型、必填、默认值、`$ref`) | +| 嵌套模型 | 支持(基本) | 支持(OpenAPI 中使用 `$ref`) | +| 性能开销 | 无 | 仅在安装并使用 Pydantic 时 | + +两种方式都支持 OpenAPI 文档。如果您需要验证功能,请使用 Pydantic。如果只需要 OpenAPI Schema 提示而不需要验证,`Body` 即可满足需求。 + + +## 重要说明 + +- **按处理函数选择启用** — 验证仅在使用 Pydantic `BaseModel` 注解参数的处理函数上运行。所有其他处理函数(使用 `Body`、`Request`、路径参数等)行为完全不变,无任何额外开销。 +- **每个处理函数只能有一个 Pydantic 请求体参数** — 每个处理函数最多只能有一个使用 Pydantic 模型注解的参数。整个请求体会被解析到这个模型中。如果需要多个模型输入,请将它们组合成一个包含嵌套字段的父模型。 +- **仅验证请求** — Robyn 根据 Pydantic 模型验证*传入*的请求体,但不验证*传出*的响应。当您返回模型实例时,它会直接序列化而不进行重新验证。这是出于性能考虑的设计决策 — 如果您构造了模型,它已经是有效的。 + + +## 下一步 + +蝙蝠侠想知道 Robyn 的处理函数是否可以分发到多个进程中执行。 + +Robyn 向他展示了方法! + +[多进程执行](/documentation/zh/api_reference/multiprocess_execution) + + diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 13a20e685..9f327c590 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -4,7 +4,7 @@ import pathlib import time from collections import defaultdict -from typing import List, Optional +from typing import List, Optional, TypedDict from integration_tests.subroutes import di_subrouter, static_router, sub_router from robyn import Headers, Request, Response, Robyn, SSEMessage, SSEResponse, WebSocketDisconnect, jsonify, serve_file, serve_html @@ -13,6 +13,13 @@ from robyn.templating import JinjaTemplate from robyn.types import Body, JsonBody, JSONResponse, Method, PathParams +try: + from pydantic import BaseModel as PydanticBaseModel + + _HAS_PYDANTIC = True +except ImportError: + _HAS_PYDANTIC = False + app = Robyn(__file__) current_file_path = pathlib.Path(__file__).parent.resolve() @@ -1514,6 +1521,132 @@ def easy_access_ws_on_close(websocket, room: str = "default"): return f"left {room}" +# ===== TypedDict Body Routes ===== + + +class TypedDictRequestBody(TypedDict): + name: str + value: int + + +class TypedDictResponseBody(TypedDict): + result: str + count: int + + +@app.post("/sync/typeddict/body", openapi_tags=["typeddict"]) +def sync_typeddict_body(data: TypedDictRequestBody) -> TypedDictResponseBody: + """Accept a TypedDict request body and return a TypedDict response""" + return {"result": data["name"], "count": data["value"]} + + +@app.post("/async/typeddict/body", openapi_tags=["typeddict"]) +async def async_typeddict_body(data: TypedDictRequestBody) -> TypedDictResponseBody: + """Accept a TypedDict request body and return a TypedDict response""" + return {"result": data["name"], "count": data["value"]} + + +@app.post("/sync/typeddict/with_request", openapi_tags=["typeddict"]) +def sync_typeddict_with_request(request: Request, data: TypedDictRequestBody): + """TypedDict body alongside Request object""" + return {"method": request.method, "name": data["name"]} + + +@app.post("/async/typeddict/with_request", openapi_tags=["typeddict"]) +async def async_typeddict_with_request(request: Request, data: TypedDictRequestBody): + """TypedDict body alongside Request object""" + return {"method": request.method, "name": data["name"]} + + +# ===== Pydantic Integration Routes ===== + +if _HAS_PYDANTIC: + + class UserCreate(PydanticBaseModel): + name: str + email: str + age: int + active: bool = True + + class Address(PydanticBaseModel): + street: str + city: str + zip_code: str + + class UserWithAddress(PydanticBaseModel): + name: str + email: str + address: Address + + @app.post("/sync/pydantic/user", openapi_tags=["pydantic"]) + def sync_pydantic_user(user: UserCreate) -> dict: + """Create a user with Pydantic validation""" + return {"name": user.name, "email": user.email, "age": user.age, "active": user.active} + + @app.post("/async/pydantic/user", openapi_tags=["pydantic"]) + async def async_pydantic_user(user: UserCreate): + return {"name": user.name, "email": user.email, "age": user.age, "active": user.active} + + @app.post("/sync/pydantic/user_with_request") + def sync_pydantic_user_with_request(request: Request, user: UserCreate): + return {"method": request.method, "name": user.name, "email": user.email} + + @app.post("/async/pydantic/user_with_request") + async def async_pydantic_user_with_request(request: Request, user: UserCreate): + return {"method": request.method, "name": user.name, "email": user.email} + + @app.post("/sync/pydantic/nested", openapi_tags=["pydantic"]) + def sync_pydantic_nested(data: UserWithAddress) -> dict: + """Create a user with nested address""" + return {"name": data.name, "city": data.address.city} + + @app.post("/async/pydantic/nested", openapi_tags=["pydantic"]) + async def async_pydantic_nested(data: UserWithAddress): + return {"name": data.name, "city": data.address.city} + + @app.put("/sync/pydantic/user") + def sync_pydantic_user_put(user: UserCreate): + return {"updated": True, "name": user.name} + + @app.put("/async/pydantic/user") + async def async_pydantic_user_put(user: UserCreate): + return {"updated": True, "name": user.name} + + @app.patch("/sync/pydantic/user") + def sync_pydantic_user_patch(user: UserCreate): + return {"patched": True, "name": user.name} + + @app.patch("/async/pydantic/user") + async def async_pydantic_user_patch(user: UserCreate): + return {"patched": True, "name": user.name} + + @app.delete("/sync/pydantic/user") + def sync_pydantic_user_delete(user: UserCreate): + return {"deleted": True, "name": user.name} + + @app.delete("/async/pydantic/user") + async def async_pydantic_user_delete(user: UserCreate): + return {"deleted": True, "name": user.name} + + @app.post("/sync/pydantic/return_model", openapi_tags=["pydantic"]) + def sync_pydantic_return_model(user: UserCreate) -> UserCreate: + """Return the validated Pydantic model directly""" + return user + + @app.post("/async/pydantic/return_model", openapi_tags=["pydantic"]) + async def async_pydantic_return_model(user: UserCreate) -> UserCreate: + return user + + @app.post("/sync/pydantic/return_list", openapi_tags=["pydantic"]) + def sync_pydantic_return_list(user: UserCreate) -> list[UserCreate]: + """Return a list of Pydantic models""" + return [user, user] + + @app.post("/async/pydantic/return_list", openapi_tags=["pydantic"]) + async def async_pydantic_return_list(user: UserCreate) -> list[UserCreate]: + return [user, user] + + def main(): app.set_response_header("server", "robyn") app.serve_directory( diff --git a/integration_tests/test_openapi.py b/integration_tests/test_openapi.py index 0e74475ad..873b69f2d 100644 --- a/integration_tests/test_openapi.py +++ b/integration_tests/test_openapi.py @@ -1,6 +1,6 @@ import pytest -from integration_tests.helpers.http_methods_helpers import get +from integration_tests.helpers.http_methods_helpers import get, json_post from robyn import Robyn @@ -263,3 +263,247 @@ def test_openapi_json_body_bare(): assert "requestBody" in openapi_spec["paths"][endpoint][route_type] assert "content" in openapi_spec["paths"][endpoint][route_type]["requestBody"] assert "application/json" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"] + + +try: + import pydantic # noqa: F401 + + _HAS_PYDANTIC = True +except ImportError: + _HAS_PYDANTIC = False + + +@pytest.mark.benchmark +@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed") +def test_openapi_pydantic_request_body(): + """Pydantic model on a regular route should produce a full JSON Schema in + requestBody — no dedicated OpenAPI route needed.""" + openapi_response = get("/openapi.json", should_check_response=False) + assert openapi_response.status_code == 200 + openapi_spec = openapi_response.json() + + endpoint = "/sync/pydantic/user" + route = openapi_spec["paths"][endpoint]["post"] + + assert route["tags"] == ["pydantic"] + assert route["description"] == "Create a user with Pydantic validation" + + assert "requestBody" in route + schema = route["requestBody"]["content"]["application/json"]["schema"] + + assert schema["type"] == "object" + assert schema["title"] == "UserCreate" + + props = schema["properties"] + assert props["name"]["type"] == "string" + assert props["name"]["title"] == "Name" + assert props["email"]["type"] == "string" + assert props["email"]["title"] == "Email" + assert props["age"]["type"] == "integer" + assert props["age"]["title"] == "Age" + assert props["active"]["type"] == "boolean" + assert props["active"]["title"] == "Active" + assert props["active"]["default"] is True + + assert set(schema["required"]) == {"name", "email", "age"} + assert "active" not in schema["required"] + + assert "responses" in route + assert "200" in route["responses"] + assert "application/json" in route["responses"]["200"]["content"] + + +@pytest.mark.benchmark +@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed") +def test_openapi_pydantic_nested_model(): + """Nested Pydantic models on a regular route should use $ref and populate + components/schemas — no dedicated OpenAPI route needed.""" + openapi_response = get("/openapi.json", should_check_response=False) + assert openapi_response.status_code == 200 + openapi_spec = openapi_response.json() + + endpoint = "/sync/pydantic/nested" + route = openapi_spec["paths"][endpoint]["post"] + + assert route["tags"] == ["pydantic"] + assert route["description"] == "Create a user with nested address" + + schema = route["requestBody"]["content"]["application/json"]["schema"] + assert schema["type"] == "object" + assert schema["title"] == "UserWithAddress" + + assert schema["properties"]["name"]["type"] == "string" + assert schema["properties"]["email"]["type"] == "string" + assert schema["properties"]["address"]["$ref"] == "#/components/schemas/Address" + + assert set(schema["required"]) == {"name", "email", "address"} + + assert "Address" in openapi_spec["components"]["schemas"] + address_schema = openapi_spec["components"]["schemas"]["Address"] + assert address_schema["type"] == "object" + assert address_schema["title"] == "Address" + + assert address_schema["properties"]["street"]["type"] == "string" + assert address_schema["properties"]["street"]["title"] == "Street" + assert address_schema["properties"]["city"]["type"] == "string" + assert address_schema["properties"]["city"]["title"] == "City" + assert address_schema["properties"]["zip_code"]["type"] == "string" + assert address_schema["properties"]["zip_code"]["title"] == "Zip Code" + + assert set(address_schema["required"]) == {"street", "city", "zip_code"} + + assert "responses" in route + assert "200" in route["responses"] + assert "application/json" in route["responses"]["200"]["content"] + + +@pytest.mark.benchmark +@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed") +def test_openapi_pydantic_return_type(): + """When a route has a Pydantic model as return type annotation, the response + schema should reflect the full Pydantic model schema, not just 'object'.""" + openapi_response = get("/openapi.json", should_check_response=False) + assert openapi_response.status_code == 200 + openapi_spec = openapi_response.json() + + endpoint = "/sync/pydantic/return_model" + route = openapi_spec["paths"][endpoint]["post"] + + assert route["tags"] == ["pydantic"] + assert route["description"] == "Return the validated Pydantic model directly" + + response_schema = route["responses"]["200"]["content"]["application/json"]["schema"] + assert response_schema["type"] == "object" + assert response_schema["title"] == "UserCreate" + assert "properties" in response_schema + assert response_schema["properties"]["name"]["type"] == "string" + assert response_schema["properties"]["age"]["type"] == "integer" + assert set(response_schema["required"]) == {"name", "email", "age"} + + +@pytest.mark.benchmark +@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed") +def test_openapi_pydantic_return_list_type(): + """When a route returns list[PydanticModel], the response schema should be + an array with items containing the full Pydantic model schema.""" + openapi_response = get("/openapi.json", should_check_response=False) + assert openapi_response.status_code == 200 + openapi_spec = openapi_response.json() + + endpoint = "/sync/pydantic/return_list" + route = openapi_spec["paths"][endpoint]["post"] + + assert route["tags"] == ["pydantic"] + assert route["description"] == "Return a list of Pydantic models" + + response_schema = route["responses"]["200"]["content"]["application/json"]["schema"] + assert response_schema["type"] == "array" + assert "items" in response_schema + + items = response_schema["items"] + assert items["type"] == "object" + assert items["title"] == "UserCreate" + assert items["properties"]["name"]["type"] == "string" + assert items["properties"]["age"]["type"] == "integer" + + +# ===== TypedDict request body tests ===== + + +@pytest.mark.benchmark +def test_openapi_typeddict_request_body(): + """TypedDict subclass used as a parameter annotation should produce a + requestBody schema in OpenAPI docs (issue #1254).""" + openapi_response = get("/openapi.json", should_check_response=False) + assert openapi_response.status_code == 200 + openapi_spec = openapi_response.json() + + endpoint = "/sync/typeddict/body" + route = openapi_spec["paths"][endpoint]["post"] + + assert route["tags"] == ["typeddict"] + assert "requestBody" in route + schema = route["requestBody"]["content"]["application/json"]["schema"] + assert "properties" in schema + assert "name" in schema["properties"] + assert "value" in schema["properties"] + assert schema["properties"]["name"]["type"] == "string" + assert schema["properties"]["value"]["type"] == "integer" + + assert "responses" in route + assert "200" in route["responses"] + response_schema = route["responses"]["200"]["content"]["application/json"]["schema"] + assert "properties" in response_schema + assert "result" in response_schema["properties"] + assert "count" in response_schema["properties"] + + +@pytest.mark.benchmark +def test_openapi_typeddict_with_request(): + """TypedDict body combined with a Request param should still produce + a requestBody schema (issue #1254).""" + openapi_response = get("/openapi.json", should_check_response=False) + assert openapi_response.status_code == 200 + openapi_spec = openapi_response.json() + + endpoint = "/sync/typeddict/with_request" + route = openapi_spec["paths"][endpoint]["post"] + + assert "requestBody" in route + schema = route["requestBody"]["content"]["application/json"]["schema"] + assert "properties" in schema + assert "name" in schema["properties"] + assert "value" in schema["properties"] + + +@pytest.mark.benchmark +def test_typeddict_body_injection_sync(): + """TypedDict-annotated parameter should receive the parsed JSON dict + at runtime, not the raw string (issue #1254).""" + response = json_post( + "/sync/typeddict/body", + json_data={"name": "alice", "value": 42}, + ) + assert response.status_code == 200 + data = response.json() + assert data["result"] == "alice" + assert data["count"] == 42 + + +@pytest.mark.benchmark +def test_typeddict_body_injection_async(): + """Async handler with TypedDict body should also receive parsed JSON.""" + response = json_post( + "/async/typeddict/body", + json_data={"name": "bob", "value": 7}, + ) + assert response.status_code == 200 + data = response.json() + assert data["result"] == "bob" + assert data["count"] == 7 + + +@pytest.mark.benchmark +def test_typeddict_body_with_request_injection(): + """TypedDict body and Request object should coexist in the same handler.""" + response = json_post( + "/sync/typeddict/with_request", + json_data={"name": "charlie", "value": 99}, + ) + assert response.status_code == 200 + data = response.json() + assert data["method"] == "POST" + assert data["name"] == "charlie" + + +@pytest.mark.benchmark +def test_typeddict_body_invalid_json(): + """Sending invalid JSON to a TypedDict-annotated handler should return 400.""" + import requests + + response = requests.post( + "http://127.0.0.1:8080/sync/typeddict/body", + data="not valid json", + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 400 diff --git a/integration_tests/test_pydantic.py b/integration_tests/test_pydantic.py new file mode 100644 index 000000000..2ceff6a39 --- /dev/null +++ b/integration_tests/test_pydantic.py @@ -0,0 +1,461 @@ +import pytest +import requests + +from integration_tests.helpers.http_methods_helpers import json_post + +BASE_URL = "http://127.0.0.1:8080" + +try: + import pydantic + + _HAS_PYDANTIC = True +except ImportError: + _HAS_PYDANTIC = False + +pytestmark = pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed") + + +def _raw_post(endpoint: str, data: str, content_type: str = "application/json") -> requests.Response: + url = f"{BASE_URL}/{endpoint.lstrip('/')}" + return requests.post(url, data=data, headers={"Content-Type": content_type}) + + +# ===== Valid Pydantic Body ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_valid_user_all_fields(function_type: str, session): + """All fields provided explicitly — every field value must round-trip correctly.""" + json_data = {"name": "Alice", "email": "alice@example.com", "age": 30, "active": False} + res = json_post(f"/{function_type}/pydantic/user", json_data=json_data) + result = res.json() + + assert result["name"] == "Alice" + assert result["email"] == "alice@example.com" + assert result["age"] == 30 + assert result["active"] is False + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_default_field_applied(function_type: str, session): + """Omitting 'active' should use the model default (True) and the handler must see it.""" + json_data = {"name": "Bob", "email": "bob@example.com", "age": 25} + res = json_post(f"/{function_type}/pydantic/user", json_data=json_data) + result = res.json() + + assert result["name"] == "Bob" + assert result["email"] == "bob@example.com" + assert result["age"] == 25 + assert result["active"] is True + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_string_to_int_coercion(function_type: str, session): + """Pydantic v2 in lax mode (default) coerces '30' string to int 30.""" + json_data = {"name": "Coerce", "email": "c@test.com", "age": "30"} + res = json_post(f"/{function_type}/pydantic/user", json_data=json_data) + result = res.json() + + assert result["name"] == "Coerce" + assert result["age"] == 30 + assert isinstance(result["age"], int) + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_extra_fields_ignored(function_type: str, session): + """Extra fields not in the model should be silently ignored (pydantic v2 default).""" + json_data = {"name": "Eve", "email": "eve@example.com", "age": 28, "extra_field": "should_be_ignored", "another": 99} + res = json_post(f"/{function_type}/pydantic/user", json_data=json_data) + result = res.json() + + assert result["name"] == "Eve" + assert result["age"] == 28 + assert "extra_field" not in result + assert "another" not in result + + +# ===== Invalid Pydantic Body — error structure verification ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_missing_single_required_field(function_type: str, session): + """Missing 'age' should produce exactly one error with correct loc, type, and msg.""" + json_data = {"name": "Charlie", "email": "charlie@example.com"} + res = json_post( + f"/{function_type}/pydantic/user", + json_data=json_data, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + + errors = result["detail"] + assert isinstance(errors, list) + assert len(errors) == 1 + + err = errors[0] + assert err["loc"] == ["age"] + assert err["type"] == "missing" + assert "required" in err["msg"].lower() + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_missing_all_required_fields(function_type: str, session): + """Sending {} should produce errors for all 3 required fields (name, email, age).""" + res = json_post( + f"/{function_type}/pydantic/user", + json_data={}, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + + errors = result["detail"] + assert isinstance(errors, list) + error_fields = {tuple(e["loc"]) for e in errors} + assert ("name",) in error_fields + assert ("email",) in error_fields + assert ("age",) in error_fields + + for err in errors: + assert err["type"] == "missing" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_wrong_type_error_detail(function_type: str, session): + """Wrong type should produce error with correct loc and a meaningful msg.""" + json_data = {"name": "Diana", "email": "diana@example.com", "age": "not_a_number"} + res = json_post( + f"/{function_type}/pydantic/user", + json_data=json_data, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + + errors = result["detail"] + age_errors = [e for e in errors if e["loc"] == ["age"]] + assert len(age_errors) == 1 + assert "int" in age_errors[0]["type"] + assert "input" in age_errors[0] + assert age_errors[0]["input"] == "not_a_number" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_multiple_type_errors(function_type: str, session): + """Multiple fields with wrong types should each produce their own error.""" + json_data = {"name": 12345, "email": True, "age": "bad"} + res = json_post( + f"/{function_type}/pydantic/user", + json_data=json_data, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + error_locs = {tuple(e["loc"]) for e in result["detail"]} + assert ("name",) in error_locs + assert ("email",) in error_locs + assert ("age",) in error_locs + + +# ===== Malformed / edge-case bodies ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_invalid_json_syntax(function_type: str, session): + """Completely invalid JSON should return 422 with 'Invalid request body' error.""" + res = _raw_post(f"/{function_type}/pydantic/user", data="not json at all {{{") + assert res.status_code == 422 + result = res.json() + assert "error" in result + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_empty_body(function_type: str, session): + """Empty body should return 422.""" + res = _raw_post(f"/{function_type}/pydantic/user", data="") + assert res.status_code == 422 + result = res.json() + assert "error" in result + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_json_array_body(function_type: str, session): + """A JSON array instead of an object should return 422.""" + res = _raw_post(f"/{function_type}/pydantic/user", data='[{"name": "X"}]') + assert res.status_code == 422 + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_null_body(function_type: str, session): + """JSON null body should return 422.""" + res = _raw_post(f"/{function_type}/pydantic/user", data="null") + assert res.status_code == 422 + + +# ===== Pydantic + Request object ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_with_request_object(function_type: str, session): + """Handler receiving both Request and Pydantic model must see both correctly.""" + json_data = {"name": "Frank", "email": "frank@example.com", "age": 35} + res = json_post(f"/{function_type}/pydantic/user_with_request", json_data=json_data) + result = res.json() + + assert result["method"] == "POST" + assert result["name"] == "Frank" + assert result["email"] == "frank@example.com" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_with_request_validation_still_works(function_type: str, session): + """Validation must still trigger 422 even when Request is in the signature.""" + json_data = {"name": "Frank"} # missing email and age + res = json_post( + f"/{function_type}/pydantic/user_with_request", + json_data=json_data, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + error_fields = {tuple(e["loc"]) for e in result["detail"]} + assert ("email",) in error_fields + assert ("age",) in error_fields + + +# ===== Nested Pydantic Models ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_nested_model_valid(function_type: str, session): + """Valid nested model should be parsed and accessible through the parent.""" + json_data = { + "name": "Grace", + "email": "grace@example.com", + "address": {"street": "123 Main St", "city": "Springfield", "zip_code": "62701"}, + } + res = json_post(f"/{function_type}/pydantic/nested", json_data=json_data) + result = res.json() + + assert result["name"] == "Grace" + assert result["city"] == "Springfield" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_nested_model_missing_nested_fields(function_type: str, session): + """Missing fields in nested model should produce errors with correct nested loc paths.""" + json_data = { + "name": "Grace", + "email": "grace@example.com", + "address": {"street": "123 Main St"}, # missing city and zip_code + } + res = json_post( + f"/{function_type}/pydantic/nested", + json_data=json_data, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + + errors = result["detail"] + error_locs = {tuple(e["loc"]) for e in errors} + assert ("address", "city") in error_locs + assert ("address", "zip_code") in error_locs + + for err in errors: + assert err["type"] == "missing" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_nested_model_missing_entirely(function_type: str, session): + """Missing the entire nested object should produce an error at the parent field.""" + json_data = {"name": "Grace", "email": "grace@example.com"} + res = json_post( + f"/{function_type}/pydantic/nested", + json_data=json_data, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + + errors = result["detail"] + address_errors = [e for e in errors if e["loc"] == ["address"]] + assert len(address_errors) == 1 + assert address_errors[0]["type"] == "missing" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_nested_model_wrong_type(function_type: str, session): + """Passing a non-object for the nested model should return 422.""" + json_data = { + "name": "Grace", + "email": "grace@example.com", + "address": "not an object", + } + res = json_post( + f"/{function_type}/pydantic/nested", + json_data=json_data, + expected_status_code=422, + should_check_response=False, + ) + assert res.status_code == 422 + result = res.json() + errors = result["detail"] + address_errors = [e for e in errors if "address" in e["loc"]] + assert len(address_errors) >= 1 + + +# ===== Pydantic with PUT ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_put_valid(function_type: str, session): + """Pydantic validation must work with PUT method.""" + json_data = {"name": "Hank", "email": "hank@example.com", "age": 40} + res = requests.put(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data) + result = res.json() + + assert result["updated"] is True + assert result["name"] == "Hank" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_put_invalid(function_type: str, session): + """PUT with invalid body must also return 422 with proper error structure.""" + json_data = {"name": "Hank"} # missing email and age + res = requests.put(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + error_fields = {tuple(e["loc"]) for e in result["detail"]} + assert ("email",) in error_fields + assert ("age",) in error_fields + + +# ===== Pydantic with PATCH ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_patch_valid(function_type: str, session): + """Pydantic validation must work with PATCH method.""" + json_data = {"name": "Iris", "email": "iris@example.com", "age": 29} + res = requests.patch(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data) + result = res.json() + + assert result["patched"] is True + assert result["name"] == "Iris" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_patch_invalid(function_type: str, session): + """PATCH with invalid body must also return 422.""" + json_data = {"name": "Iris"} # missing email and age + res = requests.patch(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + error_fields = {tuple(e["loc"]) for e in result["detail"]} + assert ("email",) in error_fields + assert ("age",) in error_fields + + +# ===== Pydantic with DELETE ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_delete_valid(function_type: str, session): + """Pydantic validation must work with DELETE method.""" + json_data = {"name": "Zara", "email": "zara@example.com", "age": 33} + res = requests.delete(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data) + result = res.json() + + assert result["deleted"] is True + assert result["name"] == "Zara" + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_delete_invalid(function_type: str, session): + """DELETE with invalid body must also return 422 with proper error structure.""" + json_data = {"name": "Zara"} # missing email and age + res = requests.delete(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + error_fields = {tuple(e["loc"]) for e in result["detail"]} + assert ("email",) in error_fields + assert ("age",) in error_fields + + +# ===== Returning Pydantic models directly ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_return_model_directly(function_type: str, session): + """Returning a Pydantic model from a handler should auto-serialize to JSON.""" + json_data = {"name": "Jack", "email": "jack@example.com", "age": 32} + res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_model", json=json_data) + + assert res.status_code == 200 + assert "application/json" in res.headers.get("content-type", "") + + result = res.json() + assert result["name"] == "Jack" + assert result["email"] == "jack@example.com" + assert result["age"] == 32 + assert result["active"] is True # default field + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_return_model_preserves_all_fields(function_type: str, session): + """Returned model should include every field, including those with defaults.""" + json_data = {"name": "Kate", "email": "kate@example.com", "age": 27, "active": False} + res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_model", json=json_data) + result = res.json() + + assert result["name"] == "Kate" + assert result["email"] == "kate@example.com" + assert result["age"] == 27 + assert result["active"] is False + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_return_model_validation_still_works(function_type: str, session): + """Validation should still trigger 422 even when the route returns a model.""" + json_data = {"name": "Kate"} # missing email and age + res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_model", json=json_data) + assert res.status_code == 422 + result = res.json() + assert result["error"] == "Validation Error" + + +# ===== Returning lists of Pydantic models ===== + + +@pytest.mark.parametrize("function_type", ["sync", "async"]) +def test_pydantic_return_list_of_models(function_type: str, session): + """Returning a list of Pydantic models should auto-serialize to a JSON array.""" + json_data = {"name": "Leo", "email": "leo@example.com", "age": 45} + res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_list", json=json_data) + + assert res.status_code == 200 + assert "application/json" in res.headers.get("content-type", "") + + result = res.json() + assert isinstance(result, list) + assert len(result) == 2 + assert result[0]["name"] == "Leo" + assert result[1]["name"] == "Leo" + assert result[0]["active"] is True diff --git a/pyproject.toml b/pyproject.toml index 0fea03b91..95d918c00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,8 @@ dependencies = [ [project.optional-dependencies] "templating" = ["jinja2 >= 3.1.6, < 4.0.0"] +"pydantic" = ["pydantic >= 2.0.0, < 3.0.0"] +"all" = ["jinja2 >= 3.1.6, < 4.0.0", "pydantic >= 2.0.0, < 3.0.0"] [project.urls] Documentation = "https://robyn.tech/" @@ -80,11 +82,14 @@ watchdog = "^6.0.0" multiprocess = "^0.70.18" uvloop = { version = "0.22.1", markers = "sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')" } jinja2 = { version = "^3.1.6", optional = true } +pydantic = { version = "^2.0.0", optional = true } rustimport = "^1.3.4" orjson = "^3.11.5" [tool.poetry.extras] templating = ["jinja2"] +pydantic = ["pydantic"] +all = ["jinja2", "pydantic"] [tool.poetry.group.dev] optional = true diff --git a/robyn/openapi.py b/robyn/openapi.py index d3640441f..a8f5981d9 100644 --- a/robyn/openapi.py +++ b/robyn/openapi.py @@ -1,17 +1,21 @@ import inspect import json +import logging import re import typing from dataclasses import asdict, dataclass, field from importlib import resources from inspect import Signature from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict +from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict, is_typeddict from robyn.responses import html from robyn.robyn import QueryParams, Response +from robyn.pydantic_support import get_pydantic_openapi_schema, is_pydantic_model from robyn.types import Body, JsonBody +_logger = logging.getLogger(__name__) + class str_typed_dict(TypedDict): key: str @@ -211,6 +215,10 @@ def add_openapi_path_obj(self, route_type: str, endpoint: str, openapi_name: str request_body = param_annotation elif issubclass(param_annotation, QueryParams): query_params = param_annotation + elif is_pydantic_model(param_annotation): + request_body = param_annotation + elif is_typeddict(param_annotation): + request_body = param_annotation if signature.return_annotation is not Signature.empty: return_annotation = signature.return_annotation @@ -223,9 +231,26 @@ def add_openapi_path_obj(self, route_type: str, endpoint: str, openapi_name: str self.openapi_spec["paths"][modified_endpoint] = {} self.openapi_spec["paths"][modified_endpoint][route_type] = path_obj + def _merge_component_schemas(self, incoming: dict): + """Merge incoming component schemas into the spec with collision detection. + + If an incoming schema name already exists and the schemas differ, + a warning is logged. The incoming schema always wins so that the + most-recently-registered route's models are used (matching the + behaviour of path registration). + """ + existing = self.openapi_spec["components"]["schemas"] + for name, schema in incoming.items(): + if name in existing and existing[name] != schema: + _logger.warning( + "OpenAPI component schema '%s' is defined by multiple models with different shapes — the later definition will be used", + name, + ) + existing[name] = schema + def add_subrouter_paths(self, subrouter_openapi: "OpenAPI"): """ - Adds the subrouter paths to main router's openapi specs + Adds the subrouter paths and component schemas to main router's openapi specs. @param subrouter_openapi: OpenAPI the OpenAPI object of the current subrouter """ @@ -233,10 +258,12 @@ def add_subrouter_paths(self, subrouter_openapi: "OpenAPI"): if self.openapi_file_override: return - paths = subrouter_openapi.openapi_spec["paths"] + for path, path_obj in subrouter_openapi.openapi_spec["paths"].items(): + self.openapi_spec["paths"][path] = path_obj - for path in paths: - self.openapi_spec["paths"][path] = paths[path] + subrouter_schemas = subrouter_openapi.openapi_spec.get("components", {}).get("schemas", {}) + if subrouter_schemas: + self._merge_component_schemas(subrouter_schemas) def get_path_obj( self, @@ -312,23 +339,26 @@ def get_path_obj( ) if request_body: - properties = {} - - request_body_annotations = request_body.__annotations__ if request_body is TypedDict else typing.get_type_hints(request_body) - - for body_item in request_body_annotations: - properties[body_item] = self.get_schema_object(body_item, request_body_annotations[body_item]) - - request_body_object = { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": properties, + if is_pydantic_model(request_body): + schema, component_schemas = get_pydantic_openapi_schema(request_body) + if component_schemas: + self._merge_component_schemas(component_schemas) + request_body_object = {"content": {"application/json": {"schema": schema}}} + else: + properties = {} + request_body_annotations = request_body.__annotations__ if request_body is TypedDict else typing.get_type_hints(request_body) + for body_item in request_body_annotations: + properties[body_item] = self.get_schema_object(body_item, request_body_annotations[body_item]) + request_body_object = { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": properties, + } } } } - } openapi_path_object["requestBody"] = request_body_object @@ -403,6 +433,13 @@ def get_schema_object(self, parameter: str, param_type: Any) -> dict: properties["items"] = self.get_schema_object(f"{parameter}_item", item_type) return properties + # check for Pydantic models + if is_pydantic_model(param_type): + schema, component_schemas = get_pydantic_openapi_schema(param_type) + if component_schemas: + self._merge_component_schemas(component_schemas) + return schema + # check for Optional type if param_type.__module__ == "typing": properties["anyOf"] = [{"type": self.get_openapi_type(param_type.__args__[0])}, {"type": "null"}] diff --git a/robyn/pydantic_support.py b/robyn/pydantic_support.py new file mode 100644 index 000000000..327937c12 --- /dev/null +++ b/robyn/pydantic_support.py @@ -0,0 +1,225 @@ +""" +Optional Pydantic integration for Robyn. + +All pydantic imports are lazy: pydantic is only loaded on the first call +to _ensure_pydantic(), so there is zero import-time overhead when pydantic +is not installed or not used. The module itself is imported at the top +level by router.py and openapi.py, but this is safe because it contains +no pydantic imports at module scope. +""" + +import inspect +from typing import Any, Optional, Tuple + +import orjson + +__all__ = [ + "is_pydantic_available", + "is_pydantic_model", + "detect_pydantic_params", + "validate_pydantic_body", + "get_pydantic_openapi_schema", + "serialize_pydantic_response", + "check_pydantic_installed_for_handler", + "PydanticBodyValidationError", + "PydanticNotInstalledError", + "MultiplePydanticBodyError", +] + +_BaseModel = None +_ValidationError = None +_pydantic_checked = False + + +def _ensure_pydantic(): + """Lazy-load pydantic classes. Called at most once.""" + global _BaseModel, _ValidationError, _pydantic_checked + if _pydantic_checked: + return + _pydantic_checked = True + try: + from pydantic import BaseModel, ValidationError + + _BaseModel = BaseModel + _ValidationError = ValidationError + except ImportError: + _BaseModel = None + _ValidationError = None + + +def is_pydantic_available() -> bool: + _ensure_pydantic() + return _BaseModel is not None + + +def is_pydantic_model(annotation) -> bool: + """Check if an annotation is a pydantic BaseModel subclass. + Returns False if pydantic is not installed or annotation is not a class.""" + _ensure_pydantic() + if _BaseModel is None: + return False + return inspect.isclass(annotation) and issubclass(annotation, _BaseModel) + + +def detect_pydantic_params(handler_params) -> dict: + """Scan pre-computed handler parameters for Pydantic BaseModel annotations. + + Accepts the ``parameters`` OrderedDict from ``inspect.signature(handler)``. + Returns a dict mapping param_name -> model_class for params annotated with + a Pydantic BaseModel subclass. Returns empty dict if pydantic is not + installed or no params use Pydantic models. + """ + _ensure_pydantic() + if _BaseModel is None: + return {} + + result = {} + for name, param in handler_params.items(): + ann = param.annotation + if ann is inspect.Parameter.empty: + continue + if inspect.isclass(ann) and issubclass(ann, _BaseModel): + result[name] = ann + return result + + +def _sanitize_errors(errors: list) -> list: + """Make pydantic error dicts JSON-serializable. + + Only copies dicts that actually contain non-serializable values. + Pydantic v2 error dicts can contain bytes, tuples, and other + non-JSON-serializable values in 'input', 'loc', and 'ctx' fields. + """ + sanitized = [] + for err in errors: + needs_copy = False + for key, val in err.items(): + if (key == "input" and isinstance(val, bytes)) or key == "loc" or (key == "ctx" and isinstance(val, dict)): + needs_copy = True + break + + if not needs_copy: + sanitized.append(err) + continue + + clean = dict(err) + if "input" in clean and isinstance(clean["input"], bytes): + clean["input"] = clean["input"].decode("utf-8", errors="replace") + if "loc" in clean: + clean["loc"] = list(clean["loc"]) + if "ctx" in clean and isinstance(clean["ctx"], dict): + clean["ctx"] = {k: str(v) for k, v in clean["ctx"].items()} + sanitized.append(clean) + return sanitized + + +def validate_pydantic_body(model_class, body: Any) -> Tuple[Any, Optional[dict]]: + """Validate request body against a Pydantic model. + + Uses model_validate_json for maximum performance — single-pass + parse+validate without an intermediate dict. model_validate_json + accepts str, bytes, and bytearray natively. + + This function is only called from the request hot path *after* + _ensure_pydantic() has already been called at registration time, + so we skip the redundant check here. + + Returns: + (validated_model_instance, None) on success + (None, error_detail_dict) on failure + """ + try: + return model_class.model_validate_json(body), None + except _ValidationError as e: + return None, { + "detail": _sanitize_errors(e.errors()), + "error": "Validation Error", + } + + +def get_pydantic_openapi_schema(model_class) -> Tuple[dict, dict]: + """Get OpenAPI-compatible JSON Schema from a Pydantic model. + + Uses ref_template so nested model references point to + #/components/schemas/{ModelName} in the OpenAPI spec. + + Returns: + (schema, component_schemas) where: + - schema: the model's JSON Schema (without $defs) + - component_schemas: dict of model_name -> schema for components/schemas + """ + _ensure_pydantic() + if _BaseModel is None or not (inspect.isclass(model_class) and issubclass(model_class, _BaseModel)): + return {}, {} + + full_schema = model_class.model_json_schema(ref_template="#/components/schemas/{model}") + component_schemas = full_schema.pop("$defs", {}) + return full_schema, component_schemas + + +def serialize_pydantic_response(res) -> Optional[str]: + """Serialize a Pydantic model (or list of models) to a JSON string. + + Returns None when *res* is not a Pydantic type so the caller can fall + through to other serialisation paths. + + This function is only called from the response hot path *after* + _ensure_pydantic() has already been called at registration time, + so we skip the redundant check here. + """ + if _BaseModel is None: + return None + if isinstance(res, _BaseModel): + return res.model_dump_json() + if isinstance(res, list) and res and isinstance(res[0], _BaseModel): + return orjson.dumps([item.model_dump(mode="python") for item in res]).decode("utf-8") + return None + + +class PydanticBodyValidationError(Exception): + """Raised at request time when Pydantic body validation fails. + Carries the serializable error dict for the 422 response.""" + + def __init__(self, error_detail: dict): + self.error_detail = error_detail + super().__init__(error_detail.get("error", "Validation Error")) + + +class PydanticNotInstalledError(ImportError): + """Raised at route registration when a handler uses a Pydantic model + but pydantic is not installed.""" + + def __init__(self, handler_name: str, param_name: str, model_name: str): + super().__init__( + f"Handler '{handler_name}' has parameter '{param_name}' annotated with " + f"Pydantic model '{model_name}', but pydantic is not installed. " + f'Install it with: pip install "robyn[pydantic]" or pip install "robyn[all]"' + ) + + +class MultiplePydanticBodyError(TypeError): + """Raised at route registration when a handler declares more than one + Pydantic body parameter.""" + + def __init__(self, handler_name: str, param_names: list): + super().__init__( + f"Handler '{handler_name}' has multiple Pydantic body parameters " + f"{param_names}. Only one Pydantic body parameter per handler is " + f"supported — the entire request body is parsed into that single model." + ) + + +def check_pydantic_installed_for_handler(handler, pydantic_params: dict): + """Validate Pydantic usage at startup. + + Raises PydanticNotInstalledError if pydantic isn't available. + Raises MultiplePydanticBodyError if more than one body param is declared. + """ + if not pydantic_params: + return + if not is_pydantic_available(): + first_param = next(iter(pydantic_params)) + model = pydantic_params[first_param] + raise PydanticNotInstalledError(handler.__name__, first_param, model.__name__) + if len(pydantic_params) > 1: + raise MultiplePydanticBodyError(handler.__name__, list(pydantic_params.keys())) diff --git a/robyn/router.py b/robyn/router.py index 609a3a959..242e57531 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from functools import wraps from types import CoroutineType -from typing import Callable, Dict, List, NamedTuple, Optional, Union +from typing import Callable, Dict, List, NamedTuple, Optional, Union, is_typeddict from robyn import status_codes from robyn._param_utils import QueryParamValidationError, parse_route_param_names, resolve_individual_params @@ -11,6 +11,13 @@ from robyn.dependency_injection import DependencyMap from robyn.jsonify import jsonify from robyn.openapi import OpenAPI +from robyn.pydantic_support import ( + PydanticBodyValidationError, + check_pydantic_installed_for_handler, + detect_pydantic_params, + serialize_pydantic_response, + validate_pydantic_body, +) from robyn.responses import FileResponse, StreamingResponse from robyn.robyn import FunctionInfo, Headers, HttpMethod, Identity, MiddlewareType, QueryParams, Request, Response, Url from robyn.types import Body, Files, FormData, IPAddress, JsonBody, Method, PathParams @@ -80,6 +87,14 @@ def _format_response( if isinstance(res, StreamingResponse): return res + pydantic_json = serialize_pydantic_response(res) + if pydantic_json is not None: + return Response( + status_code=status_codes.HTTP_200_OK, + headers=Headers({"Content-Type": "application/json"}), + description=pydantic_json, + ) + if isinstance(res, (dict, list)): return Response( status_code=status_codes.HTTP_200_OK, @@ -124,11 +139,12 @@ def add_route( # type: ignore exception_handler: Optional[Callable], injected_dependencies: dict, ) -> Union[Callable, CoroutineType]: - # Pre-compute at registration time + # Pre-compute handler signature ONCE at registration time. + # This avoids calling inspect.signature() on every request. route_param_names = parse_route_param_names(endpoint) + handler_params = inspect.signature(handler).parameters + handler_param_names = set(handler_params.keys()) - # Warn if the route declares :param names the handler doesn't use - handler_param_names = set(inspect.signature(handler).parameters.keys()) unused_route_params = route_param_names - handler_param_names if unused_route_params: _logger.warning( @@ -138,12 +154,23 @@ def add_route( # type: ignore handler.__name__, ) + # Detect Pydantic model params once at registration (zero cost if not used) + pydantic_params = detect_pydantic_params(handler_params) + check_pydantic_installed_for_handler(handler, pydantic_params) + + if pydantic_params and route_type in (HttpMethod.GET, HttpMethod.HEAD): + _logger.warning( + "Handler '%s' on %s '%s' uses Pydantic body parameter(s) %s, but %s requests typically do not carry a request body", + handler.__name__, + route_type.name, + endpoint, + list(pydantic_params.keys()), + route_type.name, + ) + def wrapped_handler(*args, **kwargs): - # In the execute functions the request is passed into *args request = next(filter(lambda it: isinstance(it, Request), args), None) - handler_params = inspect.signature(handler).parameters - if not request or (len(handler_params) == 1 and next(iter(handler_params)) is Request): return handler(*args, **kwargs) @@ -186,6 +213,25 @@ def wrapped_handler(*args, **kwargs): type_filtered_params[handler_param_name] = getattr(request, "body") elif issubclass(handler_param_type, QueryParams): type_filtered_params[handler_param_name] = getattr(request, "query_params") + elif is_typeddict(handler_param_type): + try: + type_filtered_params[handler_param_name] = request.json() + except ValueError as e: + return Response( + status_code=status_codes.HTTP_400_BAD_REQUEST, + headers=Headers({"Content-Type": "application/json"}), + description=jsonify({"error": f"Invalid JSON body: {e}"}), + ) + + # Phase 1.5: Pydantic model body params (only runs if handler uses pydantic) + if pydantic_params: + for param_name, model_class in pydantic_params.items(): + if param_name in type_filtered_params: + continue + validated, error = validate_pydantic_body(model_class, request.body) + if error is not None: + raise PydanticBodyValidationError(error) + type_filtered_params[param_name] = validated # Phase 2: Reserved-name request components request_components = { @@ -237,6 +283,12 @@ async def async_inner_handler(*args, **kwargs): headers=Headers({"Content-Type": "text/plain"}), description=str(err), ) + except PydanticBodyValidationError as err: + response = Response( + status_code=status_codes.HTTP_422_UNPROCESSABLE_ENTITY, + headers=Headers({"Content-Type": "application/json"}), + description=jsonify(err.error_detail), + ) except Exception as err: if exception_handler is None: raise @@ -257,6 +309,12 @@ def inner_handler(*args, **kwargs): headers=Headers({"Content-Type": "text/plain"}), description=str(err), ) + except PydanticBodyValidationError as err: + response = Response( + status_code=status_codes.HTTP_422_UNPROCESSABLE_ENTITY, + headers=Headers({"Content-Type": "application/json"}), + description=jsonify(err.error_detail), + ) except Exception as err: if exception_handler is None: raise @@ -265,8 +323,7 @@ def inner_handler(*args, **kwargs): ) return response - # these are the arguments - params = dict(inspect.signature(handler).parameters) + params = dict(handler_params) new_injected_dependencies = {} for dependency in injected_dependencies: diff --git a/robyn/types.py b/robyn/types.py index ac4740734..2542e64e9 100644 --- a/robyn/types.py +++ b/robyn/types.py @@ -78,4 +78,4 @@ def create_user(request: Request, data: MyBody): pass -__all__ = ["JSONResponse", "Body", "JsonBody", "QueryParamValidationError"] +__all__ = ["JSONResponse", "Body", "JsonBody", "QueryParamValidationError", "Directory", "PathParams", "Method", "FormData", "Files", "IPAddress"] From 538404994a7660966979e2d3e567fc131d8acde4 Mon Sep 17 00:00:00 2001 From: eason <85663565+mango766@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:08:31 +0800 Subject: [PATCH 046/106] fix: replace deprecated logger.warn(), use ValueError, fix docs syntax error (#1329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace deprecated `logger.warn()` with `logger.warning()` in robyn/logger.py (deprecated since Python 3.3) - Replace generic `Exception` with `ValueError` in argument_parser.py for invalid argument combinations - Fix syntax error `.username` → `user.username` in authentication docs (en + zh), closes #1162 Co-authored-by: easonysliu --- .../src/pages/documentation/en/example_app/authentication.mdx | 2 +- .../src/pages/documentation/zh/example_app/authentication.mdx | 2 +- robyn/argument_parser.py | 4 ++-- robyn/logger.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs_src/src/pages/documentation/en/example_app/authentication.mdx b/docs_src/src/pages/documentation/en/example_app/authentication.mdx index 725ef1aa8..9b6d2ecf8 100644 --- a/docs_src/src/pages/documentation/en/example_app/authentication.mdx +++ b/docs_src/src/pages/documentation/en/example_app/authentication.mdx @@ -105,7 +105,7 @@ def authenticate_user(db: Session, username: str, password: str): if not verify_password(password, user.hashed_password): return False - created_token = create_access_token(data={"sub": .username}) + created_token = create_access_token(data={"sub": user.username}) return created_token diff --git a/docs_src/src/pages/documentation/zh/example_app/authentication.mdx b/docs_src/src/pages/documentation/zh/example_app/authentication.mdx index 7f3534d6c..48fdce499 100644 --- a/docs_src/src/pages/documentation/zh/example_app/authentication.mdx +++ b/docs_src/src/pages/documentation/zh/example_app/authentication.mdx @@ -103,7 +103,7 @@ def authenticate_user(db: Session, username: str, password: str): if not verify_password(password, user.hashed_password): return False - created_token = create_access_token(data={"sub": .username}) + created_token = create_access_token(data={"sub": user.username}) return created_token diff --git a/robyn/argument_parser.py b/robyn/argument_parser.py index 2232c55b2..f81519c79 100644 --- a/robyn/argument_parser.py +++ b/robyn/argument_parser.py @@ -118,10 +118,10 @@ def __init__(self) -> None: break if self.fast and self.dev: - raise Exception("--fast and --dev shouldn't be used together") + raise ValueError("--fast and --dev shouldn't be used together") if self.dev and (self.processes != 1 or self.workers != 1): - raise Exception("--processes and --workers shouldn't be used with --dev") + raise ValueError("--processes and --workers shouldn't be used with --dev") if self.dev and self.log_level is None: self.log_level = "DEBUG" diff --git a/robyn/logger.py b/robyn/logger.py index fd72648a2..61840e6da 100644 --- a/robyn/logger.py +++ b/robyn/logger.py @@ -54,7 +54,7 @@ def warn( bold: bool = False, underline: bool = False, ): - self.logger.warn(self._format_msg(msg, color, bold, underline), *args) + self.logger.warning(self._format_msg(msg, color, bold, underline), *args) def info( self, From 4a2104dbdbeef8384fdc6c0180b97b2ddabde749 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Fri, 13 Mar 2026 19:06:44 +0000 Subject: [PATCH 047/106] Release 0.81.0 Made-with: Cursor --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a52d35159..e08d74f91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.80.0" +version = "0.81.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 65a01e3e9..ff8b35029 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.80.0" +version = "0.81.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index 95d918c00..f95105915 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.80.0" +version = "0.81.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -69,7 +69,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.80.0" +version = "0.81.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 904d566a2946a6e33c0b3a0ee816082d8eb7601a Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:30:42 +0000 Subject: [PATCH 048/106] feat: make robyn 200% faster (#1282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: make robyn 50% faster * perf: const route fast path + CachedResponse + strip debug from hot path - Add const route fast path in default_service handler that bypasses request parsing, Python GIL, scope_local, and middleware entirely - Introduce CachedResponse with Bytes body (Arc clone) and flat Vec headers instead of DashMap, eliminating expensive per-request cloning - Build HttpResponse directly from CachedResponse, skipping the Response→ResponseType→Responder chain - Optimize 0-param handlers to skip Request→Python conversion - Extract response type extraction into dedicated inline functions - Strip all debug!() logging from hot path (server, executors, request parsing, response extraction, headers) - Try Response before StreamingResponse in extraction (common case first) Benchmarks (wrk, 4t/128c/15s, 1 proc × 8 workers): Plaintext: 137,618 req/s (was 91,169) → +51% and 22% faster than Granian JSON: 128,977 req/s (was 106,589) → +21% and 24% faster than Granian Made-with: Cursor * perf: pre-bake headers, flat HashMap const routes, socket tuning - Pre-bake global response headers into CachedResponse at server startup via bake_global_headers(), eliminating per-request DashMap iteration of global headers entirely - Add flat HashMap lookup for const routes, bypassing matchit regex matching and RwLock read-acquire overhead - Increase socket listen backlog from 1024 to 16384 - Enable TCP_NODELAY on the listener socket - Enable non-blocking mode on the listener socket - to_http_response() no longer takes global_headers parameter (headers are pre-baked) Robyn is now 42% faster than socketify.py and 67% faster than Granian: Plaintext: 137,036 req/s (socketify: 96,644, Granian: 82,408) JSON: 128,617 req/s (socketify: 84,286, Granian: 77,814) Made-with: Cursor * fix: const route fast path respects middleware correctness The fast path now only activates when no global middlewares are registered. When middlewares exist (e.g. after_request that sets response headers), const routes fall through to the normal index() path so middleware runs correctly. - Add has_global_middlewares() to MiddlewareRouter - Guard fast path with !has_middlewares boolean (computed once at startup) - Re-add const_router to index() for the middleware-present code path - Only bake global headers into CachedResponse when no middlewares exist All 324 integration tests pass. Made-with: Cursor * fix: correctness fixes for const route fast path and response finalization - Track middleware presence with AtomicBool (set on add_global_middleware and add_route), read dynamically per-request instead of captured once at startup. Covers both global and route-scoped middleware. - Fast path honors excluded_response_headers_paths: serves without baked global headers when the const route's path is excluded. CachedResponse tracks route_header_count to distinguish route headers from baked global headers. - Before-middleware Response now flows through the shared finalization path (global header extension + excluded path check) instead of returning immediately. - excluded_response_headers_paths now skips the global header extend instead of extending then clearing, preserving route-set headers. All 324 integration tests pass. Made-with: Cursor * fix formatting * fix: remove dead __anext__ check, log generator errors instead of swallowing AsyncGeneratorWrapper in Python wraps async generators into sync iterators, so Rust never sees __anext__. The hasattr check was dead code. Generator errors other than StopIteration are now logged instead of silently returning None. Made-with: Cursor * fix formatting * robyn faster * update * update * update * add local CI script to catch failures before pushing Made-with: Cursor * update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- Cargo.lock | 1 + Cargo.toml | 1 + ci-local.sh | 120 ++++++ .../en/api_reference/advanced_routing.mdx | 4 +- .../api_reference/architecture_deep_dive.mdx | 6 +- integration_tests/test_middlewares.py | 17 + src/asyncio.rs | 31 ++ src/blocking.rs | 172 ++++++++ src/callbacks.rs | 404 ++++++++++++++++++ src/conversion.rs | 25 ++ src/executors/mod.rs | 120 ++---- src/lib.rs | 13 +- src/routers/const_router.rs | 183 ++++++-- src/routers/middleware_router.rs | 15 +- src/runtime.rs | 327 ++++++++++++++ src/server.rs | 276 ++++++------ src/shared_socket.rs | 8 +- src/types/headers.rs | 5 - src/types/request.rs | 27 +- src/types/response.rs | 170 +------- src/websockets/mod.rs | 6 +- 21 files changed, 1493 insertions(+), 438 deletions(-) create mode 100755 ci-local.sh create mode 100644 src/asyncio.rs create mode 100644 src/blocking.rs create mode 100644 src/callbacks.rs create mode 100644 src/conversion.rs create mode 100644 src/runtime.rs diff --git a/Cargo.lock b/Cargo.lock index e08d74f91..af3e19479 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1523,6 +1523,7 @@ dependencies = [ "actix-web", "actix-web-actors", "anyhow", + "crossbeam-channel", "dashmap", "futures", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index ff8b35029..6b86bbd52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ serde_json = "1.0.109" once_cell = "1.8.0" actix-multipart = "0.6.1" parking_lot = "0.12.3" +crossbeam-channel = "0.5" [features] io-uring = ["actix-web/experimental-io-uring"] diff --git a/ci-local.sh b/ci-local.sh new file mode 100755 index 000000000..2c5081bd8 --- /dev/null +++ b/ci-local.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +FAILED=() +PASSED=() +SKIPPED=() + +run_step() { + local name="$1" + shift + echo -e "\n${CYAN}── $name ──${NC}" + echo -e "${YELLOW}$ $*${NC}" + if "$@"; then + PASSED+=("$name") + echo -e "${GREEN}✓ $name${NC}" + else + FAILED+=("$name") + echo -e "${RED}✗ $name${NC}" + fi +} + +skip_step() { + local name="$1" + local reason="$2" + SKIPPED+=("$name ($reason)") + echo -e "\n${YELLOW}── $name [SKIPPED: $reason] ──${NC}" +} + +usage() { + echo "Usage: $0 [rust|lint|python|all|fix]" + echo "" + echo "Mirrors the GitHub Actions CI workflows locally." + echo "" + echo " rust Rust CI: cargo check, test, fmt --check, clippy" + echo " lint Lint PR: ruff check, isort --check-only" + echo " python Python CI: nox test suite (current Python version)" + echo " all Everything (default)" + echo " fix Auto-fix: cargo fmt, ruff --fix, isort" + exit 0 +} + +# ── rust-CI.yml ─────────────────────────────────────────────────────────────── +run_rust() { + echo -e "\n${CYAN}═══ Rust CI (.github/workflows/rust-CI.yml) ═══${NC}" + run_step "cargo check" cargo check + run_step "cargo test" cargo test + run_step "cargo fmt" cargo fmt --check + run_step "cargo clippy" cargo clippy +} + +# ── lint-pr.yml ─────────────────────────────────────────────────────────────── +run_lint() { + echo -e "\n${CYAN}═══ Lint PR (.github/workflows/lint-pr.yml) ═══${NC}" + + if command -v ruff &>/dev/null; then + run_step "ruff check" ruff check . + else + skip_step "ruff check" "ruff not installed (pip install ruff)" + fi + + if command -v isort &>/dev/null; then + run_step "isort check" isort --check-only --diff . + else + skip_step "isort check" "isort not installed (pip install isort)" + fi +} + +# ── python-CI.yml ───────────────────────────────────────────────────────────── +run_python() { + echo -e "\n${CYAN}═══ Python CI (.github/workflows/python-CI.yml) ═══${NC}" + local pyver + pyver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + + if command -v nox &>/dev/null; then + run_step "nox (python $pyver)" nox --non-interactive --error-on-missing-interpreter -p "$pyver" + else + skip_step "nox tests" "nox not installed (pip install nox)" + fi +} + +# ── fix mode ────────────────────────────────────────────────────────────────── +run_fix() { + echo -e "\n${CYAN}═══ Auto-fix ═══${NC}" + run_step "cargo fmt" cargo fmt + command -v ruff &>/dev/null && run_step "ruff fix" ruff check --fix . || skip_step "ruff fix" "not installed" + command -v isort &>/dev/null && run_step "isort fix" isort . || skip_step "isort fix" "not installed" +} + +# ── main ────────────────────────────────────────────────────────────────────── +MODE="${1:-all}" + +case "$MODE" in + rust) run_rust ;; + lint) run_lint ;; + python) run_python ;; + fix) run_fix ;; + all) run_rust; run_lint; run_python ;; + -h|--help|help) usage ;; + *) echo "Unknown mode: $MODE"; usage ;; +esac + +# ── summary ─────────────────────────────────────────────────────────────────── +echo -e "\n${CYAN}═══ Summary ═══${NC}" +for s in "${PASSED[@]+"${PASSED[@]}"}"; do echo -e " ${GREEN}✓${NC} $s"; done +for s in "${SKIPPED[@]+"${SKIPPED[@]}"}"; do echo -e " ${YELLOW}⊘${NC} $s"; done +for s in "${FAILED[@]+"${FAILED[@]}"}"; do echo -e " ${RED}✗${NC} $s"; done + +if [ ${#FAILED[@]} -gt 0 ]; then + echo -e "\n${RED}CI would fail: ${#FAILED[@]} check(s) failed.${NC}" + exit 1 +else + echo -e "\n${GREEN}All checks passed. Safe to push.${NC}" + exit 0 +fi diff --git a/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx b/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx index 9f10c9095..985c111e8 100644 --- a/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx @@ -185,7 +185,9 @@ The parameter injection system works in two phases: - Use `const=True` for responses that never change. These are cached in Rust memory and served without Python execution. + Use `const=True` for responses that never change. These are cached in Rust memory and the handler function is never re-executed after startup. + + When no middleware is registered, const routes take a fast path served entirely from the Rust layer without entering Python at all. When middleware is registered (including global before-request and after-request handlers), const routes still serve the cached response but middleware executes normally for every request. This means const routes are always safe to use alongside middleware. diff --git a/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx b/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx index e7cd19473..fa7ea124a 100644 --- a/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx +++ b/docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx @@ -118,7 +118,7 @@ Robyn includes a "const routes" optimization for static responses: - When you mark a route as `const`, Robyn can serve the response directly from the Rust layer without invoking Python at all. + When you mark a route as `const`, Robyn caches the response in Rust memory and never re-executes the Python handler. If no middleware is registered, const routes are served entirely from the Rust layer without entering Python at all. When middleware is present, the cached response is still used but before-request and after-request middleware execute normally. @@ -128,8 +128,8 @@ Robyn includes a "const routes" optimization for static responses: def health_check(): return {"status": "healthy"} - # Subsequent requests are served directly from Rust - # bypassing Python entirely + # Without middleware: served directly from Rust, bypassing Python entirely + # With middleware: cached response is used, but middleware still runs ``` diff --git a/integration_tests/test_middlewares.py b/integration_tests/test_middlewares.py index 172a5007b..340466073 100644 --- a/integration_tests/test_middlewares.py +++ b/integration_tests/test_middlewares.py @@ -28,3 +28,20 @@ def test_global_middleware(session): def test_response_in_before_middleware(session): r = get("/sync/middlewares/401", should_check_response=False) assert r.status_code == 401 + + +@pytest.mark.benchmark +@pytest.mark.parametrize( + "route", + [ + "/sync/str/const", + "/async/str/const", + "/sync/dict/const", + "/async/dict/const", + "/sync/response/const", + "/async/response/const", + ], +) +def test_global_middleware_applied_to_const_routes(route: str, session): + r = get(route) + assert r.headers.get("global_after") == "global_after_request", f"Global after-request middleware was not applied to const route {route}" diff --git a/src/asyncio.rs b/src/asyncio.rs new file mode 100644 index 000000000..3c52bb5bd --- /dev/null +++ b/src/asyncio.rs @@ -0,0 +1,31 @@ +use pyo3::{prelude::*, sync::PyOnceLock}; +use std::convert::Into; + +static CONTEXTVARS: PyOnceLock> = PyOnceLock::new(); +static CONTEXT: PyOnceLock> = PyOnceLock::new(); + +fn contextvars(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { + Ok(CONTEXTVARS + .get_or_try_init(py, || py.import("contextvars").map(Into::into))? + .bind(py)) +} + +#[allow(dead_code)] +pub(crate) fn empty_context(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { + Ok(CONTEXT + .get_or_try_init(py, || { + contextvars(py)? + .getattr("Context")? + .call0() + .map(std::convert::Into::into) + })? + .bind(py)) +} + +#[inline(always)] +pub(crate) fn copy_context(py: Python) -> PyResult> { + unsafe { + let ptr = pyo3::ffi::PyContext_CopyCurrent(); + Ok(Bound::from_owned_ptr_or_err(py, ptr)?.unbind()) + } +} diff --git a/src/blocking.rs b/src/blocking.rs new file mode 100644 index 000000000..8576b8932 --- /dev/null +++ b/src/blocking.rs @@ -0,0 +1,172 @@ +use crossbeam_channel as channel; +use pyo3::prelude::*; +use std::{ + sync::{atomic, Arc}, + thread, time, +}; + +pub(crate) struct BlockingTask { + inner: Box, +} + +impl BlockingTask { + #[inline] + pub fn new(inner: T) -> BlockingTask + where + T: FnOnce(Python) + Send + 'static, + { + Self { + inner: Box::new(inner), + } + } + + #[inline(always)] + pub fn run(self, py: Python) { + (self.inner)(py); + } +} + +pub(crate) enum BlockingRunner { + Empty, + Mono(BlockingRunnerMono), + Pool(BlockingRunnerPool), +} + +impl BlockingRunner { + pub fn new(max_threads: usize, idle_timeout: u64) -> Self { + match max_threads { + 0 => Self::Empty, + 1 => Self::Mono(BlockingRunnerMono::new()), + _ => Self::Pool(BlockingRunnerPool::new(max_threads, idle_timeout)), + } + } + + #[inline] + pub fn run(&self, task: T) -> Result<(), channel::SendError> + where + T: FnOnce(Python) + Send + 'static, + { + match self { + Self::Mono(runner) => runner.run(task), + Self::Pool(runner) => runner.run(task), + Self::Empty => Ok(()), + } + } +} + +pub(crate) struct BlockingRunnerMono { + queue: channel::Sender, +} + +impl BlockingRunnerMono { + pub fn new() -> Self { + let (qtx, qrx) = channel::unbounded(); + let ret = Self { queue: qtx }; + thread::spawn(move || blocking_worker(qrx)); + + ret + } + + #[inline] + pub fn run(&self, task: T) -> Result<(), channel::SendError> + where + T: FnOnce(Python) + Send + 'static, + { + self.queue.send(BlockingTask::new(task)) + } +} + +pub(crate) struct BlockingRunnerPool { + birth: time::Instant, + queue: channel::Sender, + tq: channel::Receiver, + threads: Arc, + tmax: usize, + idle_timeout: time::Duration, + spawning: atomic::AtomicBool, + spawn_tick: atomic::AtomicU64, +} + +impl BlockingRunnerPool { + pub fn new(max_threads: usize, idle_timeout: u64) -> Self { + let (qtx, qrx) = channel::unbounded(); + let ret = Self { + queue: qtx, + tq: qrx.clone(), + threads: Arc::new(1.into()), + tmax: max_threads, + birth: time::Instant::now(), + spawning: false.into(), + spawn_tick: 0.into(), + idle_timeout: time::Duration::from_secs(idle_timeout), + }; + + // always spawn the first thread + thread::spawn(move || blocking_worker(qrx)); + + ret + } + + #[inline(always)] + fn spawn_thread(&self) { + let tick = self.birth.elapsed().as_micros() as u64; + if tick - self.spawn_tick.load(atomic::Ordering::Relaxed) < 350 { + return; + } + if self + .spawning + .compare_exchange( + false, + true, + atomic::Ordering::Relaxed, + atomic::Ordering::Relaxed, + ) + .is_err() + { + return; + } + + let queue = self.tq.clone(); + let tcount = self.threads.clone(); + let timeout = self.idle_timeout; + thread::spawn(move || { + tcount.fetch_add(1, atomic::Ordering::Release); + blocking_worker_idle(queue, timeout); + tcount.fetch_sub(1, atomic::Ordering::Release); + }); + + self.spawn_tick.store( + self.birth.elapsed().as_micros() as u64, + atomic::Ordering::Relaxed, + ); + self.spawning.store(false, atomic::Ordering::Relaxed); + } + + #[inline] + pub fn run(&self, task: T) -> Result<(), channel::SendError> + where + T: FnOnce(Python) + Send + 'static, + { + self.queue.send(BlockingTask::new(task))?; + if self.queue.len() > 1 && self.threads.load(atomic::Ordering::Acquire) < self.tmax { + self.spawn_thread(); + } + Ok(()) + } +} + +fn blocking_worker(queue: channel::Receiver) { + Python::attach(|py| { + while let Ok(task) = py.detach(|| queue.recv()) { + task.run(py); + } + }); +} + +fn blocking_worker_idle(queue: channel::Receiver, timeout: time::Duration) { + Python::attach(|py| { + while let Ok(task) = py.detach(|| queue.recv_timeout(timeout)) { + task.run(py); + } + }); +} diff --git a/src/callbacks.rs b/src/callbacks.rs new file mode 100644 index 000000000..9408b3fa8 --- /dev/null +++ b/src/callbacks.rs @@ -0,0 +1,404 @@ +use pyo3::{exceptions::PyStopIteration, prelude::*, IntoPyObjectExt}; +use std::sync::{atomic, Arc, OnceLock, RwLock}; +use tokio::sync::Notify; + +use crate::conversion::FutureResultToPy; + +#[pyclass(frozen, freelist = 128, module = "robyn._robyn")] +pub(crate) struct PyEmptyAwaitable; + +#[pymethods] +impl PyEmptyAwaitable { + fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __next__(&self) -> Option<()> { + None + } +} + +#[pyclass(frozen, module = "robyn._robyn")] +pub(crate) struct PyDoneAwaitable { + result: PyResult>, +} + +impl PyDoneAwaitable { + pub(crate) fn new(result: PyResult>) -> Self { + Self { result } + } +} + +#[pymethods] +impl PyDoneAwaitable { + fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __next__(&self, py: Python) -> PyResult> { + self.result + .as_ref() + .map_err(|v| v.clone_ref(py)) + .map(|v| Err(PyStopIteration::new_err(v.clone_ref(py))))? + } +} + +#[pyclass(frozen, module = "robyn._robyn")] +pub(crate) struct PyErrAwaitable { + err: PyErr, +} + +impl PyErrAwaitable { + pub(crate) fn new(err: PyErr) -> Self { + Self { err } + } +} + +#[pymethods] +impl PyErrAwaitable { + fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __next__(&self, py: Python) -> PyResult<()> { + Err(self.err.clone_ref(py)) + } +} + +#[pyclass(frozen, module = "robyn._robyn")] +pub(crate) struct PyIterAwaitable { + result: OnceLock>>, +} + +impl PyIterAwaitable { + pub(crate) fn new() -> Self { + Self { + result: OnceLock::new(), + } + } + + #[inline] + pub(crate) fn set_result(pyself: Py, py: Python, result: FutureResultToPy) { + _ = pyself + .get() + .result + .set(result.into_pyobject(py).map(Bound::unbind)); + pyself.drop_ref(py); + } +} + +#[pymethods] +impl PyIterAwaitable { + fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __next__(&self, py: Python) -> PyResult>> { + if let Some(res) = self.result.get() { + return res + .as_ref() + .map_err(|err| err.clone_ref(py)) + .map(|v| Err(PyStopIteration::new_err(v.clone_ref(py))))?; + } + + Ok(Some(py.None())) + } +} + +#[repr(u8)] +enum PyFutureAwaitableState { + Pending = 0, + Completed = 1, + Cancelled = 2, +} + +#[pyclass(frozen, module = "robyn._robyn")] +pub(crate) struct PyFutureAwaitable { + state: atomic::AtomicU8, + result: OnceLock>>, + event_loop: Py, + cancel_tx: Arc, + cancel_msg: OnceLock>, + py_block: atomic::AtomicBool, + ack: RwLock, Py)>>, +} + +impl PyFutureAwaitable { + pub(crate) fn new(event_loop: Py) -> Self { + Self { + state: atomic::AtomicU8::new(PyFutureAwaitableState::Pending as u8), + result: OnceLock::new(), + event_loop, + cancel_tx: Arc::new(Notify::new()), + cancel_msg: OnceLock::new(), + py_block: true.into(), + ack: RwLock::new(None), + } + } + + pub fn to_spawn(self, py: Python) -> PyResult<(Py, Arc)> { + let cancel_tx = self.cancel_tx.clone(); + Ok((Py::new(py, self)?, cancel_tx)) + } + + pub(crate) fn set_result(pyself: Py, py: Python, result: FutureResultToPy) { + let rself = pyself.get(); + + _ = rself + .result + .set(result.into_pyobject(py).map(Bound::unbind)); + if rself + .state + .compare_exchange( + PyFutureAwaitableState::Pending as u8, + PyFutureAwaitableState::Completed as u8, + atomic::Ordering::Release, + atomic::Ordering::Relaxed, + ) + .is_err() + { + pyself.drop_ref(py); + return; + } + + { + let ack = rself.ack.read().unwrap(); + if let Some((cb, ctx)) = &*ack { + _ = rself.event_loop.clone_ref(py).call_method( + py, + pyo3::intern!(py, "call_soon_threadsafe"), + (cb, pyself.clone_ref(py)), + Some(ctx.bind(py)), + ); + } + } + pyself.drop_ref(py); + } +} + +#[pymethods] +impl PyFutureAwaitable { + fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> { + pyself + } + + fn __next__(pyself: PyRef<'_, Self>) -> PyResult>> { + if pyself.state.load(atomic::Ordering::Acquire) == PyFutureAwaitableState::Completed as u8 { + let py = pyself.py(); + return pyself + .result + .get() + .unwrap() + .as_ref() + .map_err(|err| err.clone_ref(py)) + .map(|v| Err(PyStopIteration::new_err(v.clone_ref(py))))?; + } + + Ok(Some(pyself)) + } + + #[getter(_asyncio_future_blocking)] + fn get_block(&self) -> bool { + self.py_block.load(atomic::Ordering::Relaxed) + } + + #[setter(_asyncio_future_blocking)] + fn set_block(&self, val: bool) { + self.py_block.store(val, atomic::Ordering::Relaxed); + } + + fn get_loop(&self, py: Python) -> Py { + self.event_loop.clone_ref(py) + } + + /// Single-callback optimization: only the most recent callback is stored. + /// This is intentional — this type is internal to the robyn runtime and in + /// practice asyncio registers at most one done-callback per future. + #[pyo3(signature = (cb, context=None))] + fn add_done_callback( + pyself: PyRef<'_, Self>, + cb: Py, + context: Option>, + ) -> PyResult<()> { + let py = pyself.py(); + let kwctx = pyo3::types::PyDict::new(py); + kwctx.set_item(pyo3::intern!(py, "context"), context)?; + + let state = pyself.state.load(atomic::Ordering::Acquire); + if state == PyFutureAwaitableState::Pending as u8 { + let mut ack = pyself.ack.write().unwrap(); + *ack = Some((cb, kwctx.unbind())); + } else { + let event_loop = pyself.event_loop.clone_ref(py); + event_loop.call_method( + py, + pyo3::intern!(py, "call_soon"), + (cb, pyself), + Some(&kwctx), + )?; + } + + Ok(()) + } + + /// Clears the single stored callback (see `add_done_callback`). + /// The `cb` argument is accepted for asyncio protocol compatibility but + /// is not used for matching — the sole stored callback is always removed. + #[allow(unused)] + fn remove_done_callback(&self, cb: Py) -> i32 { + let mut ack = self.ack.write().unwrap(); + if ack.is_some() { + *ack = None; + 1 + } else { + 0 + } + } + + #[allow(unused)] + #[pyo3(signature = (msg=None))] + fn cancel(pyself: PyRef<'_, Self>, msg: Option>) -> bool { + if pyself + .state + .compare_exchange( + PyFutureAwaitableState::Pending as u8, + PyFutureAwaitableState::Cancelled as u8, + atomic::Ordering::Release, + atomic::Ordering::Relaxed, + ) + .is_err() + { + return false; + } + + if let Some(cancel_msg) = msg { + _ = pyself.cancel_msg.set(cancel_msg); + } + pyself.cancel_tx.notify_one(); + + let ack = pyself.ack.read().unwrap(); + if let Some((cb, ctx)) = &*ack { + let py = pyself.py(); + let event_loop = pyself.event_loop.clone_ref(py); + let cb = cb.clone_ref(py); + let ctx = ctx.clone_ref(py); + drop(ack); + + let _ = event_loop.call_method( + py, + pyo3::intern!(py, "call_soon"), + (cb, pyself), + Some(ctx.bind(py)), + ); + } + + true + } + + fn done(&self) -> bool { + self.state.load(atomic::Ordering::Acquire) != PyFutureAwaitableState::Pending as u8 + } + + fn result(&self, py: Python) -> PyResult> { + let state = self.state.load(atomic::Ordering::Acquire); + + if state == PyFutureAwaitableState::Completed as u8 { + return self + .result + .get() + .unwrap() + .as_ref() + .map(|v| v.clone_ref(py)) + .map_err(|err| err.clone_ref(py)); + } + if state == PyFutureAwaitableState::Cancelled as u8 { + let msg = self + .cancel_msg + .get() + .unwrap_or(&"Future cancelled.".into_py_any(py).unwrap()) + .clone_ref(py); + return Err(pyo3::exceptions::asyncio::CancelledError::new_err(msg)); + } + Err(pyo3::exceptions::asyncio::InvalidStateError::new_err( + "Result is not ready.", + )) + } + + fn exception(&self, py: Python) -> PyResult> { + let state = self.state.load(atomic::Ordering::Acquire); + + if state == PyFutureAwaitableState::Completed as u8 { + return self + .result + .get() + .unwrap() + .as_ref() + .map(|_| py.None()) + .map_err(|err| err.clone_ref(py)); + } + if state == PyFutureAwaitableState::Cancelled as u8 { + let msg = self + .cancel_msg + .get() + .unwrap_or(&"Future cancelled.".into_py_any(py).unwrap()) + .clone_ref(py); + return Err(pyo3::exceptions::asyncio::CancelledError::new_err(msg)); + } + Err(pyo3::exceptions::asyncio::InvalidStateError::new_err( + "Exception is not set.", + )) + } +} + +#[pyclass(frozen)] +pub(crate) struct PyFutureDoneCallback { + pub cancel_tx: Arc, +} + +#[pymethods] +impl PyFutureDoneCallback { + pub fn __call__(&self, fut: Bound) -> PyResult<()> { + let py = fut.py(); + + if { + fut.getattr(pyo3::intern!(py, "cancelled"))? + .call0()? + .is_truthy() + } + .unwrap_or(false) + { + self.cancel_tx.notify_one(); + } + + Ok(()) + } +} + +#[pyclass(frozen)] +pub(crate) struct PyFutureResultSetter; + +#[pymethods] +impl PyFutureResultSetter { + pub fn __call__(&self, target: Bound, value: Bound) { + let _ = target.call1((value,)); + } +} diff --git a/src/conversion.rs b/src/conversion.rs new file mode 100644 index 000000000..05c1b4d67 --- /dev/null +++ b/src/conversion.rs @@ -0,0 +1,25 @@ +use pyo3::prelude::*; + +// Adapted for robyn - returns Py directly +pub(crate) enum FutureResultToPy { + None, + Err(PyResult<()>), + Value(Py), +} + +impl<'p> IntoPyObject<'p> for FutureResultToPy { + type Target = PyAny; + type Output = Bound<'p, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'p>) -> Result { + match self { + Self::None => Ok(py.None().into_bound(py)), + Self::Err(res) => Err(res.err().unwrap()), + Self::Value(val) => { + let bound = val.bind(py); + Ok(bound.clone()) + } + } + } +} diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 7a5204224..d811b1892 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -1,12 +1,9 @@ #[deny(clippy::if_same_then_else)] -/// This is the module that has all the executor functions -/// i.e. the functions that have the responsibility of parsing and executing functions. pub mod web_socket_executors; use std::sync::Arc; use anyhow::Result; -use log::debug; use pyo3::prelude::*; use pyo3::{BoundObject, IntoPyObject}; use pyo3_async_runtimes::TaskLocals; @@ -29,6 +26,12 @@ where for<'py> >::Error: std::fmt::Debug, { let handler = function.handler.bind(py).downcast()?; + + // 0-param handlers: skip Request→Python conversion entirely + if function.number_of_params == 0 { + return handler.call0(); + } + let kwargs = function.kwargs.bind(py); let function_args: Py = function_args .clone() @@ -41,16 +44,13 @@ where })? .into_any() .unbind(); - debug!("Function args: {:?}", function_args); match function.number_of_params { - 0 => handler.call0(), 1 => { if pyo3::types::PyDictMethods::get_item(kwargs, "global_dependencies") .is_ok_and(|it| !it.is_none()) || pyo3::types::PyDictMethods::get_item(kwargs, "router_dependencies") .is_ok_and(|it| !it.is_none()) - // these are reserved keywords { handler.call((), Some(kwargs)) } else { @@ -92,7 +92,6 @@ where } else { Python::with_gil(|py| -> Result { let output = get_function_output(function, py, input)?; - debug!("Middleware output: {:?}", output); match output.extract::() { Ok(response) => Ok(MiddlewareReturn::Response(response)), @@ -121,6 +120,11 @@ where for<'py> >::Error: std::fmt::Debug, { let handler = function.handler.bind(py).downcast()?; + + if function.number_of_params == 0 { + return handler.call0(); + } + let kwargs = function.kwargs.bind(py); let first_arg: Py = first_arg .clone() @@ -144,12 +148,9 @@ where })? .into_any() .unbind(); - debug!("Function args: {:?}, {:?}", first_arg, second_arg); match function.number_of_params { - 0 => handler.call0(), 1 => { - // If function only has 1 parameter, pass only the response (second_arg) for backward compatibility if pyo3::types::PyDictMethods::get_item(kwargs, "global_dependencies") .is_ok_and(|it| !it.is_none()) || pyo3::types::PyDictMethods::get_item(kwargs, "router_dependencies") @@ -161,8 +162,6 @@ where } } 2 => { - // If function has 2 parameters, pass both request and response - // Check if there are dependencies to pass via kwargs if pyo3::types::PyDictMethods::get_item(kwargs, "global_dependencies") .is_ok_and(|it| !it.is_none()) || pyo3::types::PyDictMethods::get_item(kwargs, "router_dependencies") @@ -205,7 +204,6 @@ pub async fn execute_after_middleware_function( } else { Python::with_gil(|py| -> Result { let output = get_function_output_with_two_args(function, py, request, response)?; - debug!("After middleware output: {:?}", output); match output.extract::() { Ok(response) => Ok(MiddlewareReturn::Response(response)), @@ -230,83 +228,48 @@ pub async fn execute_http_function( })? .await?; - Python::with_gil(|py| -> PyResult { - debug!( - "Output object type: {}", - output - .bind(py) - .get_type() - .name() - .map(|n| n.to_string()) - .unwrap_or_else(|_| "unknown".to_string()) - ); - // Try to extract as StreamingResponse first, then as Response - match output.extract::(py) { - Ok(streaming_response) => { - debug!("Successfully extracted as StreamingResponse"); - Ok(ResponseType::Streaming(streaming_response)) - } - Err(streaming_err) => { - debug!("Failed to extract as StreamingResponse: {}", streaming_err); - match output.extract::(py) { - Ok(response) => { - debug!("Successfully extracted as Response"); - Ok(ResponseType::Standard(response)) - } - Err(response_err) => { - debug!("Failed to extract as Response: {}", response_err); - Err(PyErr::new::( - "Function must return a Response or StreamingResponse", - )) - } - } - } - } - }) + Python::with_gil(|py| extract_response_type(output, py)) } else { - Python::with_gil(|py| -> PyResult { + Python::with_gil(|py| { let output = get_function_output(function, py, request)?; - debug!( - "Output object type: {}", - output - .get_type() - .name() - .map(|n| n.to_string()) - .unwrap_or_else(|_| "unknown".to_string()) - ); - // Try to extract as StreamingResponse first, then as Response - match output.extract::() { - Ok(streaming_response) => { - debug!("Successfully extracted as StreamingResponse"); - Ok(ResponseType::Streaming(streaming_response)) - } - Err(streaming_err) => { - debug!("Failed to extract as StreamingResponse: {}", streaming_err); - match output.extract::() { - Ok(response) => { - debug!("Successfully extracted as Response"); - Ok(ResponseType::Standard(response)) - } - Err(response_err) => { - debug!("Failed to extract as Response: {}", response_err); - Err(PyErr::new::( - "Function must return a Response or StreamingResponse", - )) - } - } - } - } + extract_response_type_bound(output) }) } } +#[inline] +fn extract_response_type(output: Py, py: Python) -> PyResult { + // Try Response first (most common case), then StreamingResponse + match output.extract::(py) { + Ok(response) => Ok(ResponseType::Standard(response)), + Err(_) => match output.extract::(py) { + Ok(streaming_response) => Ok(ResponseType::Streaming(streaming_response)), + Err(_) => Err(PyErr::new::( + "Function must return a Response or StreamingResponse", + )), + }, + } +} + +#[inline] +fn extract_response_type_bound(output: pyo3::Bound<'_, pyo3::PyAny>) -> PyResult { + match output.extract::() { + Ok(response) => Ok(ResponseType::Standard(response)), + Err(_) => match output.extract::() { + Ok(streaming_response) => Ok(ResponseType::Streaming(streaming_response)), + Err(_) => Err(PyErr::new::( + "Function must return a Response or StreamingResponse", + )), + }, + } +} + pub async fn execute_startup_handler( event_handler: Option>, task_locals: &TaskLocals, ) -> Result<()> { if let Some(function) = event_handler { if function.is_async { - debug!("Startup event handler async"); Python::with_gil(|py| { pyo3_async_runtimes::into_future_with_locals( task_locals, @@ -315,7 +278,6 @@ pub async fn execute_startup_handler( })? .await?; } else { - debug!("Startup event handler"); Python::with_gil(|py| function.handler.call0(py))?; } } diff --git a/src/lib.rs b/src/lib.rs index 4ed95de84..c1bc1b323 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,11 @@ +mod asyncio; +mod blocking; +mod callbacks; +mod conversion; mod executors; mod io_helpers; mod routers; +mod runtime; mod server; mod shared_socket; mod types; @@ -53,6 +58,12 @@ pub fn robyn(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - pyo3::prepare_freethreaded_python(); + // Register awaitable types + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) } diff --git a/src/routers/const_router.rs b/src/routers/const_router.rs index c77b3b370..4d260ebe0 100644 --- a/src/routers/const_router.rs +++ b/src/routers/const_router.rs @@ -1,14 +1,17 @@ +use actix_http::StatusCode; +use actix_web::{web::Bytes, HttpResponse, HttpResponseBuilder}; use parking_lot::RwLock; use std::collections::HashMap; use std::sync::Arc; use crate::executors::execute_http_function; +use crate::types::cookie::Cookies; use crate::types::function_info::FunctionInfo; +use crate::types::headers::Headers; use crate::types::request::Request; use crate::types::response::Response; use crate::types::HttpMethod; use anyhow::Context; -use log::debug; use matchit::Router as MatchItRouter; use pyo3::{Bound, PyErr, Python}; @@ -16,15 +19,71 @@ use anyhow::{Error, Result}; use crate::routers::Router; -type RouteMap = RwLock>; +/// Pre-built response for const routes — zero per-request allocation. +/// Headers (including global response headers) are baked in at startup. +#[derive(Clone)] +pub struct CachedResponse { + pub status: StatusCode, + pub headers: Arc>, + pub body: Bytes, + route_header_count: usize, +} + +impl CachedResponse { + fn from_response(response: &Response) -> Self { + let mut headers = Vec::new(); + for entry in response.headers.headers.iter() { + let (key, values) = entry.pair(); + for value in values { + headers.push((key.clone(), value.clone())); + } + } + for (name, cookie) in &response.cookies.cookies { + if let Ok(header_value) = cookie.to_header_value(name) { + headers.push(("set-cookie".to_string(), header_value)); + } + } + let route_header_count = headers.len(); + Self { + status: StatusCode::from_u16(response.status_code) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + headers: Arc::new(headers), + body: Bytes::from(response.description.clone()), + route_header_count, + } + } + + #[inline(always)] + pub fn to_http_response(&self) -> HttpResponse { + let mut builder = HttpResponseBuilder::new(self.status); + for (k, v) in self.headers.as_ref() { + builder.append_header((k.as_str(), v.as_str())); + } + builder.body(self.body.clone()) + } + + #[inline(always)] + pub fn to_http_response_without_global_headers(&self) -> HttpResponse { + let mut builder = HttpResponseBuilder::new(self.status); + for (k, v) in self.headers.as_ref().iter().take(self.route_header_count) { + builder.append_header((k.as_str(), v.as_str())); + } + builder.body(self.body.clone()) + } +} + +type RouteMap = RwLock>; + +/// Fast const-route lookup table: exact path → CachedResponse. +/// Uses a simple HashMap (no regex, no path params, no RwLock per lookup). +type FastMap = RwLock>; -/// Contains the thread safe hashmaps of different routes pub struct ConstRouter { routes: HashMap>, + fast_routes: HashMap>, } impl Router for ConstRouter { - /// Doesn't allow query params/body/etc as variables cannot be "memoized"/"const"ified fn add_route<'py>( &self, _py: Python, @@ -34,6 +93,11 @@ impl Router for ConstRouter { event_loop: Option>, ) -> Result<(), Error> { let table = Arc::clone(self.routes.get(route_type).context("No relevant map")?); + let fast_table = Arc::clone( + self.fast_routes + .get(route_type) + .context("No relevant fast map")?, + ); let route = route.to_string(); let event_loop = @@ -43,11 +107,11 @@ impl Router for ConstRouter { let output = execute_http_function(&Request::default(), &function) .await .unwrap(); - debug!("This is the result of the output {:?}", output); - // Const routes only support standard responses, not streaming match output { crate::types::response::ResponseType::Standard(response) => { - table.write().insert(route, response).unwrap(); + let cached = CachedResponse::from_response(&response); + table.write().insert(route.clone(), cached.clone()).unwrap(); + fast_table.write().insert(route, cached); } crate::types::response::ResponseType::Streaming(_) => { return Err(PyErr::new::( @@ -63,49 +127,94 @@ impl Router for ConstRouter { } fn get_route(&self, route_method: &HttpMethod, route: &str) -> Option { - let table = self.routes.get(route_method)?; - let route_map = table.read(); - - match route_map.at(route) { - Ok(res) => Some(res.value.clone()), - Err(_) => None, + let cached = self.get_cached_route(route_method, route)?; + let mut resp = Response { + status_code: cached.status.as_u16(), + response_type: "text".to_string(), + headers: Headers::new(None), + description: cached.body.to_vec(), + file_path: None, + cookies: Cookies::new(), + }; + for (k, v) in cached.headers.as_ref() { + resp.headers.set(k.clone(), v.clone()); } + Some(resp) } } impl ConstRouter { pub fn new() -> Self { let mut routes = HashMap::new(); - routes.insert(HttpMethod::GET, Arc::new(RwLock::new(MatchItRouter::new()))); - routes.insert( + let mut fast_routes = HashMap::new(); + for method in [ + HttpMethod::GET, HttpMethod::POST, - Arc::new(RwLock::new(MatchItRouter::new())), - ); - routes.insert(HttpMethod::PUT, Arc::new(RwLock::new(MatchItRouter::new()))); - routes.insert( + HttpMethod::PUT, HttpMethod::DELETE, - Arc::new(RwLock::new(MatchItRouter::new())), - ); - routes.insert( HttpMethod::PATCH, - Arc::new(RwLock::new(MatchItRouter::new())), - ); - routes.insert( HttpMethod::HEAD, - Arc::new(RwLock::new(MatchItRouter::new())), - ); - routes.insert( HttpMethod::OPTIONS, - Arc::new(RwLock::new(MatchItRouter::new())), - ); - routes.insert( HttpMethod::CONNECT, - Arc::new(RwLock::new(MatchItRouter::new())), - ); - routes.insert( HttpMethod::TRACE, - Arc::new(RwLock::new(MatchItRouter::new())), - ); - Self { routes } + ] { + routes.insert(method.clone(), Arc::new(RwLock::new(MatchItRouter::new()))); + fast_routes.insert(method, Arc::new(RwLock::new(HashMap::new()))); + } + Self { + routes, + fast_routes, + } + } + + /// Bake global response headers into all cached responses. + /// Called once at server start, after global headers are set. + pub fn bake_global_headers(&self, global_headers: &Headers) { + let mut extra_headers: Vec<(String, String)> = Vec::new(); + for entry in global_headers.headers.iter() { + let (key, values) = entry.pair(); + for value in values { + extra_headers.push((key.clone(), value.clone())); + } + } + if extra_headers.is_empty() { + return; + } + for (method, fast_table) in &self.fast_routes { + let mut map = fast_table.write(); + for cached in map.values_mut() { + let mut combined = cached.headers.as_ref().clone(); + combined.extend(extra_headers.iter().cloned()); + cached.headers = Arc::new(combined); + } + + if let Some(route_table) = self.routes.get(method) { + let mut new_router = MatchItRouter::new(); + for (route, cached) in map.iter() { + let _ = new_router.insert(route.clone(), cached.clone()); + } + *route_table.write() = new_router; + } + } + } + + /// Fast lookup: tries exact HashMap first, falls back to matchit for parameterized/wildcard routes. + #[inline(always)] + pub fn get_cached_route( + &self, + route_method: &HttpMethod, + route: &str, + ) -> Option { + let fast_table = self.fast_routes.get(route_method)?; + { + let map = fast_table.read(); + if let Some(cached) = map.get(route) { + return Some(cached.clone()); + } + } + + let route_table = self.routes.get(route_method)?; + let router = route_table.read(); + router.at(route).ok().map(|matched| matched.value.clone()) } } diff --git a/src/routers/middleware_router.rs b/src/routers/middleware_router.rs index fa0277798..1c48999d8 100644 --- a/src/routers/middleware_router.rs +++ b/src/routers/middleware_router.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::RwLock; use anyhow::{Context, Error, Result}; @@ -10,10 +11,10 @@ use crate::types::function_info::{FunctionInfo, MiddlewareType}; type RouteMap = RwLock>; -/// Contains the thread safe hashmaps of different routes pub struct MiddlewareRouter { globals: HashMap>>, routes: HashMap, + has_middleware: AtomicBool, } impl Router<(FunctionInfo, HashMap), MiddlewareType> for MiddlewareRouter { @@ -28,6 +29,7 @@ impl Router<(FunctionInfo, HashMap), MiddlewareType> for Middlew let table = self.routes.get(route_type).context("No relevant map")?; table.write().unwrap().insert(route.to_string(), function)?; + self.has_middleware.store(true, Ordering::Release); Ok(()) } @@ -66,7 +68,11 @@ impl MiddlewareRouter { MiddlewareType::AfterRequest, RwLock::new(MatchItRouter::new()), ); - Self { globals, routes } + Self { + globals, + routes, + has_middleware: AtomicBool::new(false), + } } pub fn add_global_middleware( @@ -80,6 +86,7 @@ impl MiddlewareRouter { .write() .unwrap() .push(function); + self.has_middleware.store(true, Ordering::Release); Ok(()) } @@ -91,4 +98,8 @@ impl MiddlewareRouter { .unwrap() .to_vec() } + + pub fn has_any_middleware(&self) -> bool { + self.has_middleware.load(Ordering::Acquire) + } } diff --git a/src/runtime.rs b/src/runtime.rs new file mode 100644 index 000000000..da2829fcf --- /dev/null +++ b/src/runtime.rs @@ -0,0 +1,327 @@ +use futures::FutureExt; +use pyo3::{prelude::*, IntoPyObjectExt}; +use std::{future::Future, sync::Arc, sync::OnceLock}; +use tokio::{runtime::Builder as RuntimeBuilder, task::JoinHandle}; + +#[cfg(unix)] +use super::callbacks::PyFutureAwaitable; +#[cfg(windows)] +use super::callbacks::{PyFutureDoneCallback, PyFutureResultSetter}; + +use super::blocking::BlockingRunner; +use super::callbacks::{PyDoneAwaitable, PyEmptyAwaitable, PyErrAwaitable, PyIterAwaitable}; +use super::conversion::FutureResultToPy; + +pub trait JoinError { + #[allow(dead_code)] + fn is_panic(&self) -> bool; +} + +pub trait Runtime: Send + 'static { + type JoinError: JoinError + Send; + type JoinHandle: Future> + Send; + + fn spawn(&self, fut: F) -> Self::JoinHandle + where + F: Future + Send + 'static; + + fn spawn_blocking(&self, task: F) + where + F: FnOnce(Python) + Send + 'static; +} + +pub trait ContextExt: Runtime { + fn py_event_loop(&self, py: Python) -> Py; +} + +pub(crate) struct RuntimeWrapper { + pub inner: tokio::runtime::Runtime, + br: Arc, + pr: Arc>, +} + +impl RuntimeWrapper { + pub fn new( + blocking_threads: usize, + py_threads: usize, + py_threads_idle_timeout: u64, + py_loop: Arc>, + ) -> Self { + Self { + inner: default_runtime(blocking_threads), + br: BlockingRunner::new(py_threads, py_threads_idle_timeout).into(), + pr: py_loop, + } + } + + pub fn with_runtime( + rt: tokio::runtime::Runtime, + py_threads: usize, + py_threads_idle_timeout: u64, + py_loop: Arc>, + ) -> Self { + Self { + inner: rt, + br: BlockingRunner::new(py_threads, py_threads_idle_timeout).into(), + pr: py_loop, + } + } + + pub fn handler(&self) -> RuntimeRef { + RuntimeRef::new( + self.inner.handle().clone(), + self.br.clone(), + self.pr.clone(), + ) + } +} + +#[derive(Clone)] +pub struct RuntimeRef { + pub inner: tokio::runtime::Handle, + innerb: Arc, + innerp: Arc>, +} + +impl RuntimeRef { + pub fn new( + rt: tokio::runtime::Handle, + br: Arc, + pyloop: Arc>, + ) -> Self { + Self { + inner: rt, + innerb: br, + innerp: pyloop, + } + } +} + +impl JoinError for tokio::task::JoinError { + fn is_panic(&self) -> bool { + tokio::task::JoinError::is_panic(self) + } +} + +impl Runtime for RuntimeRef { + type JoinError = tokio::task::JoinError; + type JoinHandle = JoinHandle<()>; + + fn spawn(&self, fut: F) -> Self::JoinHandle + where + F: Future + Send + 'static, + { + self.inner.spawn(fut) + } + + #[inline] + fn spawn_blocking(&self, task: F) + where + F: FnOnce(Python) + Send + 'static, + { + _ = self.innerb.run(task); + } +} + +impl ContextExt for RuntimeRef { + fn py_event_loop(&self, py: Python) -> Py { + self.innerp.clone_ref(py) + } +} + +fn default_runtime(blocking_threads: usize) -> tokio::runtime::Runtime { + RuntimeBuilder::new_current_thread() + .max_blocking_threads(blocking_threads) + .enable_all() + .build() + .unwrap() +} + +#[inline(always)] +pub(crate) fn empty_future_into_py(py: Python) -> PyResult> { + PyEmptyAwaitable.into_bound_py_any(py) +} + +#[inline(always)] +pub(crate) fn done_future_into_py( + py: Python, + result: PyResult>, +) -> PyResult> { + PyDoneAwaitable::new(result).into_bound_py_any(py) +} + +#[inline(always)] +pub(crate) fn err_future_into_py(py: Python, err: PyErr) -> PyResult> { + PyErrAwaitable::new(err).into_bound_py_any(py) +} + +// NOTE: ~55% faster than pyo3_asyncio.future_into_py +#[allow(dead_code, unused_must_use)] +pub(crate) fn future_into_py_iter(rt: R, py: Python, fut: F) -> PyResult> +where + R: Runtime + ContextExt + Clone, + F: Future + Send + 'static, +{ + let aw = Py::new(py, PyIterAwaitable::new())?; + let py_fut = aw.clone_ref(py); + let rth = rt.clone(); + + rt.spawn(async move { + let result = fut.await; + rth.spawn_blocking(move |py| PyIterAwaitable::set_result(aw, py, result)); + }); + + Ok(py_fut.into_any().into_bound(py)) +} + +// NOTE: ~38% faster than pyo3_asyncio.future_into_py +#[allow(unused_must_use)] +#[cfg(unix)] +pub(crate) fn future_into_py_futlike(rt: R, py: Python, fut: F) -> PyResult> +where + R: Runtime + ContextExt + Clone, + F: Future + Send + 'static, +{ + let event_loop = rt.py_event_loop(py); + let (aw, cancel_tx) = PyFutureAwaitable::new(event_loop).to_spawn(py)?; + let py_fut = aw.clone_ref(py); + let rth = rt.clone(); + + rt.spawn(async move { + tokio::select! { + biased; + result = fut => rth.spawn_blocking(move |py| PyFutureAwaitable::set_result(aw, py, result)), + () = cancel_tx.notified() => rth.spawn_blocking(move |py| aw.drop_ref(py)), + } + }); + + Ok(py_fut.into_any().into_bound(py)) +} + +#[allow(unused_must_use)] +#[cfg(windows)] +pub(crate) fn future_into_py_futlike(rt: R, py: Python, fut: F) -> PyResult> +where + R: Runtime + ContextExt + Clone, + F: Future + Send + 'static, +{ + let event_loop = rt.py_event_loop(py); + let event_loop_ref = event_loop.clone_ref(py); + let cancel_tx = Arc::new(tokio::sync::Notify::new()); + let rth = rt.clone(); + + let py_fut = event_loop.call_method0(py, pyo3::intern!(py, "create_future"))?; + py_fut.call_method1( + py, + pyo3::intern!(py, "add_done_callback"), + (PyFutureDoneCallback { + cancel_tx: cancel_tx.clone(), + },), + )?; + let fut_ref = py_fut.clone_ref(py); + + rt.spawn(async move { + tokio::select! { + biased; + result = fut => { + rth.spawn_blocking(move |py| { + let pyres = result.into_pyobject(py).map(Bound::unbind); + let resolved: PyResult<()> = match pyres { + Ok(val) => { + let cb = fut_ref.getattr(py, pyo3::intern!(py, "set_result")); + match cb { + Ok(cb) => { + let _ = event_loop_ref.call_method1( + py, + pyo3::intern!(py, "call_soon_threadsafe"), + (PyFutureResultSetter, cb, val), + ); + Ok(()) + } + Err(e) => Err(e), + } + } + Err(err) => { + (|| -> PyResult<()> { + let cb = fut_ref.getattr(py, pyo3::intern!(py, "set_exception"))?; + let val = err.into_py_any(py)?; + let _ = event_loop_ref.call_method1( + py, + pyo3::intern!(py, "call_soon_threadsafe"), + (PyFutureResultSetter, cb, val), + ); + Ok(()) + })() + } + }; + if let Err(err) = resolved { + log::error!("Failed to resolve Python future: {}", err); + } + fut_ref.drop_ref(py); + event_loop_ref.drop_ref(py); + }); + }, + () = cancel_tx.notified() => { + rth.spawn_blocking(move |py| { + fut_ref.drop_ref(py); + event_loop_ref.drop_ref(py); + }); + } + } + }); + + Ok(py_fut.into_bound(py)) +} + +static SHARED_BLOCKING_RUNNER: OnceLock> = OnceLock::new(); + +fn shared_blocking_runner() -> Arc { + SHARED_BLOCKING_RUNNER + .get_or_init(|| Arc::new(BlockingRunner::new(1, 30))) + .clone() +} + +pub fn future_into_py(py: Python, fut: F) -> PyResult> +where + F: Future> + Send + 'static, +{ + match tokio::runtime::Handle::try_current() { + Ok(rt_handle) => { + let asyncio = py.import("asyncio")?; + let event_loop = asyncio + .call_method0("get_running_loop") + .or_else(|_| asyncio.call_method0("new_event_loop"))?; + let event_loop: Py = event_loop.unbind(); + + let blocking_runner = shared_blocking_runner(); + + let rt_ref = RuntimeRef::new(rt_handle, blocking_runner, Arc::new(event_loop)); + + let wrapped_fut = async move { + match fut.await { + Ok(()) => FutureResultToPy::None, + Err(e) => FutureResultToPy::Err(Err(PyErr::new::< + pyo3::exceptions::PyRuntimeError, + _, + >(format!( + "Future error: {}", + e + )))), + } + }; + + future_into_py_futlike(rt_ref, py, wrapped_fut) + } + Err(_) => { + let py_fut = fut.map(|result| { + result.map_err(|e| { + PyErr::new::(format!( + "Future error: {}", + e + )) + }) + }); + pyo3_async_runtimes::tokio::future_into_py(py, py_fut) + } + } +} diff --git a/src/server.rs b/src/server.rs index b4a9e3f68..a82b931b8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,6 +9,7 @@ use crate::routers::Router; use crate::routers::http_router::HttpRouter; use crate::routers::{middleware_router::MiddlewareRouter, web_socket_router::WebSocketRouter}; use crate::shared_socket::SocketHeld; +use crate::types::cookie::Cookies; use crate::types::function_info::{FunctionInfo, MiddlewareType}; use crate::types::headers::Headers; use crate::types::request::Request; @@ -28,8 +29,7 @@ use actix_files::Files; use actix_http::KeepAlive; use actix_web::*; -// pyO3 module -use log::{debug, error}; +use log::error; use once_cell::sync::OnceCell; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -95,7 +95,6 @@ impl Server { .compare_exchange(false, true, SeqCst, Relaxed) .is_err() { - debug!("Robyn is already running..."); return Ok(()); } @@ -136,13 +135,15 @@ impl Server { thread::spawn(move || { actix_web::rt::System::new().block_on(async move { - debug!("The number of workers is {}", workers); - let task_locals = Python::with_gil(|py| TASK_LOCALS.get().unwrap().clone_ref(py)); execute_startup_handler(startup_handler, &task_locals) .await .unwrap(); + if !middleware_router.has_any_middleware() { + const_router.bake_global_headers(&global_response_headers); + } + HttpServer::new(move || { let mut app = App::new(); @@ -215,33 +216,62 @@ impl Server { ); } - debug!("Max payload size is {}", max_payload_size); - app.app_data(web::PayloadConfig::new(max_payload_size)) .default_service(web::route().to( move |router: web::Data>, const_router: web::Data>, middleware_router: web::Data>, payload: web::Payload, - global_request_headers, - global_response_headers, - response_headers_exclude_paths, - req| { + global_request_headers: web::Data>, + global_response_headers: web::Data>, + response_headers_exclude_paths: web::Data< + Option>, + >, + req: HttpRequest| async move { + // Fast path: const routes bypass request parsing, Python, and middleware + // Only safe when no middlewares are registered (checked dynamically via AtomicBool) + if !middleware_router.has_any_middleware() { + if let Ok(http_method) = + HttpMethod::from_actix_method(req.method()) + { + if let Some(cached) = const_router + .get_cached_route(&http_method, req.uri().path()) + { + if let Some(ref excluded) = + *response_headers_exclude_paths.get_ref() + { + if excluded.contains(&req.uri().path().to_owned()) { + return cached + .to_http_response_without_global_headers(); + } + } + return cached.to_http_response(); + } + } + } + + // Normal path: dynamic routes (and const routes when middlewares exist) require Python + let req_ref = req.clone(); let task_locals = Python::with_gil(|py| TASK_LOCALS.get().unwrap().clone_ref(py)); - pyo3_async_runtimes::tokio::scope_local(task_locals, async move { - index( - router, - payload, - const_router, - middleware_router, - global_request_headers, - global_response_headers, - response_headers_exclude_paths, - req, - ) - .await - }) + let response = pyo3_async_runtimes::tokio::scope_local( + task_locals, + async move { + index( + router, + const_router, + payload, + middleware_router, + global_request_headers, + global_response_headers, + response_headers_exclude_paths, + req, + ) + .await + }, + ) + .await; + response.respond_to(&req_ref) }, )) }) @@ -258,25 +288,8 @@ impl Server { let event_loop = event_loop.call_method0("run_forever"); if event_loop.is_err() { - debug!("Ctrl c handler"); - - // executing this from the same file (and not creating a function -- like startup handler) - // to fix an issue that arises when a new async function is spooled up. - - // if we create a function & move the code, the function won't run s & raises the warning: - // "unused implementer of `futures_util::Future` that must be used futures do nothing - // unless you await or poll them." - - // but, adding `.await` raises the error "await is used inside non-async function, - // which is not an async context". - - // which can only be solved by creating a new async function -- hence, resorting - // to this solution - if let Some(function) = shutdown_handler { if function.is_async { - debug!("Shutdown event handler async"); - let task_locals = Python::with_gil(|py| TASK_LOCALS.get().unwrap().clone_ref(py)); @@ -290,8 +303,6 @@ impl Server { ) .unwrap(); } else { - debug!("Shutdown event handler"); - Python::with_gil(|py| function.handler.call0(py))?; } } @@ -364,7 +375,6 @@ impl Server { function: &FunctionInfo, is_const: bool, ) { - debug!("Route added for {:?} {} ", route_type, route); let asyncio = py.import("asyncio").unwrap(); let event_loop = asyncio.call_method0("get_event_loop").unwrap(); @@ -378,7 +388,7 @@ impl Server { ) { Ok(_) => (), Err(e) => { - debug!("Error adding const route {}", e); + log::debug!("Error adding const route {}", e); } } } else { @@ -388,7 +398,7 @@ impl Server { { Ok(_) => (), Err(e) => { - debug!("Error adding route {}", e); + log::debug!("Error adding route {}", e); } } } @@ -419,10 +429,6 @@ impl Server { endpoint_prefixed_with_method.push_str(route); - debug!( - "MiddleWare Route added for {:?} {} ", - middleware_type, &endpoint_prefixed_with_method - ); Python::with_gil(|py| { self.middleware_router .add_route( @@ -452,13 +458,11 @@ impl Server { /// Add a new startup handler pub fn add_startup_handler(&mut self, function: FunctionInfo) { self.startup_handler = Some(Arc::new(function)); - debug!("Added startup handler {:?}", self.startup_handler); } /// Add a new shutdown handler pub fn add_shutdown_handler(&mut self, function: FunctionInfo) { self.shutdown_handler = Some(Arc::new(function)); - debug!("Added shutdown handler {:?}", self.shutdown_handler); } } @@ -468,66 +472,85 @@ impl Default for Server { } } -/// This is our service handler. It receives a Request, routes on it -/// path, and returns a Future of a Response. -#[allow(clippy::too_many_arguments)] async fn index( router: web::Data>, - payload: web::Payload, const_router: web::Data>, + payload: web::Payload, middleware_router: web::Data>, global_request_headers: web::Data>, global_response_headers: web::Data>, excluded_response_headers_paths: web::Data>>, req: HttpRequest, ) -> ResponseType { - // Check if the HTTP method is supported if !HttpMethod::is_supported(req.method()) { return ResponseType::Standard(Response::method_not_allowed(None)); } + let http_method = match HttpMethod::from_actix_method(req.method()) { + Ok(method) => method, + Err(_) => return ResponseType::Standard(Response::method_not_allowed(None)), + }; + let mut request: Request = - Request::from_actix_request(&req, payload, &global_request_headers).await; + match Request::from_actix_request(&req, payload, &global_request_headers).await { + Ok(r) => r, + Err(e) => { + error!("Failed to parse request for `{}`: {}", req.path(), e); + return ResponseType::Standard(Response::internal_server_error(None)); + } + }; let route = format!("{}{}", req.method(), request.url.path); // Before middleware - // Global - let mut before_middlewares = + let before_middlewares = middleware_router.get_global_middlewares(&MiddlewareType::BeforeRequest); - // Route specific - if let Some((function, route_params)) = - middleware_router.get_route(&MiddlewareType::BeforeRequest, &route) - { - before_middlewares.push(function); - request.path_params = route_params; - } - for before_middleware in before_middlewares { - request = match execute_middleware_function(&request, &before_middleware).await { - Ok(MiddlewareReturn::Request(r)) => r, - Ok(MiddlewareReturn::Response(r)) => { - // If a before middleware returns a response, we abort the request and return the response - return ResponseType::Standard(r); - } - Err(e) => { - error!( - "Error while executing before middleware function for endpoint `{}`: {}", - request.url.path, - get_traceback(e.downcast_ref::().unwrap()) - ); - return ResponseType::Standard(Response::internal_server_error(None)); - } - }; + let route_before = middleware_router.get_route(&MiddlewareType::BeforeRequest, &route); + + let mut early_response: Option = None; + if !before_middlewares.is_empty() || route_before.is_some() { + let mut all_before = before_middlewares; + if let Some((function, route_params)) = route_before { + all_before.push(function); + request.path_params = route_params; + } + for before_middleware in all_before { + request = match execute_middleware_function(&request, &before_middleware).await { + Ok(MiddlewareReturn::Request(r)) => r, + Ok(MiddlewareReturn::Response(r)) => { + early_response = Some(r); + break; + } + Err(e) => { + let msg = match e.downcast_ref::() { + Some(py_err) => get_traceback(py_err), + None => format!("{e:?}"), + }; + error!( + "Error executing before middleware for `{}`: {}", + request.url.path, msg + ); + return ResponseType::Standard(Response::internal_server_error(None)); + } + }; + } } - // Route execution - let http_method = match HttpMethod::from_actix_method(req.method()) { - Ok(method) => method, - Err(_) => return ResponseType::Standard(Response::method_not_allowed(None)), - }; - - let mut response = if let Some(res) = const_router.get_route(&http_method, &request.url.path) { - ResponseType::Standard(res) + let mut response = if let Some(r) = early_response { + ResponseType::Standard(r) + } else if let Some(cached) = const_router.get_cached_route(&http_method, &request.url.path) { + let mut resp = Response { + status_code: cached.status.as_u16(), + response_type: "text".to_string(), + headers: Headers::new(None), + description: cached.body.to_vec(), + file_path: None, + cookies: Cookies::new(), + }; + for (k, v) in cached.headers.as_ref() { + resp.headers.set(k.clone(), v.clone()); + } + ResponseType::Standard(resp) } else if let Some((function, route_params)) = router.get_route(&http_method, &request.url.path) { request.path_params = route_params; @@ -535,11 +558,10 @@ async fn index( Ok(r) => r, Err(e) => { error!( - "Error while executing route function for endpoint `{}`: {}", + "Error executing route function for `{}`: {}", request.url.path, get_traceback(&e) ); - ResponseType::Standard(Response::internal_server_error(None)) } } @@ -547,64 +569,56 @@ async fn index( ResponseType::Standard(Response::not_found(None)) }; - debug!("OG Response : {:?}", response); - - response.headers_mut().extend(&global_response_headers); + let is_excluded = excluded_response_headers_paths + .get_ref() + .as_ref() + .is_some_and(|paths| paths.contains(&request.url.path)); - match &excluded_response_headers_paths.get_ref() { - None => {} - Some(excluded_response_headers_paths) => { - if excluded_response_headers_paths.contains(&request.url.path.to_owned()) { - response.headers_mut().clear(); - } - } + if !is_excluded { + response.headers_mut().extend(&global_response_headers); } - debug!("Extended Response : {:?}", response); - // After middleware - // Global - let mut after_middlewares = - middleware_router.get_global_middlewares(&MiddlewareType::AfterRequest); - // Route specific - if let Some((function, _)) = middleware_router.get_route(&MiddlewareType::AfterRequest, &route) - { - after_middlewares.push(function); - } - for after_middleware in after_middlewares { - // Middleware only works with standard responses - if let ResponseType::Standard(std_response) = response { - response = - match execute_after_middleware_function(&request, &std_response, &after_middleware) - .await + let after_middlewares = middleware_router.get_global_middlewares(&MiddlewareType::AfterRequest); + let route_after = middleware_router.get_route(&MiddlewareType::AfterRequest, &route); + + if !after_middlewares.is_empty() || route_after.is_some() { + let mut all_after = after_middlewares; + if let Some((function, _)) = route_after { + all_after.push(function); + } + for after_middleware in all_after { + if let ResponseType::Standard(std_response) = response { + response = match execute_after_middleware_function( + &request, + &std_response, + &after_middleware, + ) + .await { Ok(MiddlewareReturn::Request(_)) => { error!("After middleware returned a request"); return ResponseType::Standard(Response::internal_server_error(None)); } - Ok(MiddlewareReturn::Response(r)) => { - debug!("Response returned: {:?}", r); - ResponseType::Standard(r) - } + Ok(MiddlewareReturn::Response(r)) => ResponseType::Standard(r), Err(e) => { + let msg = match e.downcast_ref::() { + Some(py_err) => get_traceback(py_err), + None => format!("{e:?}"), + }; error!( - "Error while executing after middleware function for endpoint `{}`: {}", - request.url.path, - get_traceback(e.downcast_ref::().unwrap()) + "Error executing after middleware for `{}`: {}", + request.url.path, msg ); return ResponseType::Standard(Response::internal_server_error(Some( &std_response.headers, ))); } }; - } else { - // Skip middleware for streaming responses - debug!("Skipping after middleware for streaming response"); + } } } - debug!("Response returned: {:?}", response); - response } diff --git a/src/shared_socket.rs b/src/shared_socket.rs index e11196eec..d021d3576 100644 --- a/src/shared_socket.rs +++ b/src/shared_socket.rs @@ -1,6 +1,5 @@ use pyo3::prelude::*; -use log::debug; use socket2::{Domain, Protocol, Socket, Type}; use std::net::{IpAddr, SocketAddr}; @@ -21,14 +20,13 @@ impl SocketHeld { Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))? }; let address = SocketAddr::new(ip, port); - debug!("{}", address); - // reuse port is not available on windows #[cfg(not(target_os = "windows"))] socket.set_reuse_port(true)?; - socket.set_reuse_address(true)?; + socket.set_nodelay(true)?; socket.bind(&address.into())?; - socket.listen(1024)?; + socket.listen(16384)?; + socket.set_nonblocking(true)?; Ok(SocketHeld { socket }) } diff --git a/src/types/headers.rs b/src/types/headers.rs index 27acf89c3..8436587a9 100644 --- a/src/types/headers.rs +++ b/src/types/headers.rs @@ -1,6 +1,5 @@ use actix_http::header::HeaderMap; use dashmap::DashMap; -use log::debug; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; use pyo3::IntoPyObject; @@ -41,12 +40,10 @@ impl Headers { } pub fn set(&mut self, key: String, value: String) { - debug!("Setting header {} to {}", key, value); self.headers.insert(key.to_lowercase(), vec![value]); } pub fn append(&mut self, key: String, value: String) { - debug!("Setting header {} to {}", key, value); self.headers .entry(key.to_lowercase()) .or_default() @@ -98,8 +95,6 @@ impl Headers { } pub fn contains(&self, key: String) -> bool { - debug!("Checking if header {} exists", key); - debug!("Headers: {:?}", self.headers); self.headers.contains_key(&key.to_lowercase()) } diff --git a/src/types/request.rs b/src/types/request.rs index 39d4f5544..7d57777fe 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -4,7 +4,6 @@ use actix_web::{ Error, HttpRequest, }; use futures_util::StreamExt as _; -use log::debug; use pyo3::types::{PyBytes, PyDict, PyList, PyString}; use pyo3::{exceptions::PyValueError, prelude::*, IntoPyObject}; use serde_json::Value; @@ -99,9 +98,7 @@ async fn handle_multipart( let mut field = item?; let mut data = Vec::new(); - // Read the field data while let Some(chunk) = field.next().await { - debug!("Chunk: {:?}", chunk); let data_chunk = chunk?; data.extend_from_slice(&data_chunk); } @@ -127,7 +124,7 @@ impl Request { req: &HttpRequest, mut payload: web::Payload, global_headers: &Headers, - ) -> Self { + ) -> Result { let mut query_params: QueryParams = QueryParams::new(); let mut form_data: HashMap = HashMap::new(); let mut files = HashMap::new(); @@ -144,7 +141,6 @@ impl Request { } let mut headers = Headers::from_actix_headers(req.headers()); - debug!("Global headers: {:?}", global_headers); headers.extend(global_headers); let body: Vec = if headers.contains(String::from("content-type")) @@ -152,16 +148,10 @@ impl Request { .get(String::from("content-type")) .is_some_and(|val| val.contains("multipart/form-data")) { - let h = headers.get(String::from("content-type")).unwrap(); - debug!("Content-Type: {:?}", h); let multipart = Multipart::new(req.headers(), payload); let mut body_local: Vec = Vec::new(); - let a = handle_multipart(multipart, &mut files, &mut form_data, &mut body_local).await; - - if let Err(e) = a { - debug!("Error handling multipart data: {:?}", e); - } + handle_multipart(multipart, &mut files, &mut form_data, &mut body_local).await?; body_local } else { @@ -173,15 +163,6 @@ impl Request { body_local.freeze().to_vec() }; - debug!("Request body: {:?}", body); - debug!("Request headers: {:?}", headers); - debug!("Request query params: {:?}", query_params); - debug!("Request form data: {:?}", form_data); - debug!("Request files: {:?}", files); - - // Normalizing Path. - // Rules: - // 1. Other than Root("/"), "/endpoint/" will be routed to "/endpoint" internally, without any client redirection. let route_path = { let mut path = req.path(); if path.ends_with("/") && path.len() > 1 { @@ -197,7 +178,7 @@ impl Request { ); let ip_addr = req.peer_addr().map(|val| val.ip().to_string()); - Self { + Ok(Self { query_params, headers, method: req.method().as_str().to_owned(), @@ -208,7 +189,7 @@ impl Request { identity: None, form_data: Some(form_data), files: Some(files), - } + }) } } diff --git a/src/types/response.rs b/src/types/response.rs index ec8b3dc6a..a92328817 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -1,7 +1,6 @@ use actix_http::{body::BoxBody, StatusCode}; use actix_web::{web::Bytes, HttpRequest, HttpResponse, HttpResponseBuilder, Responder}; use futures::Stream; -use log::debug; use pyo3::{ exceptions::PyIOError, prelude::*, @@ -76,7 +75,7 @@ impl Responder for Response { response_builder.append_header(("Set-Cookie", header_value)); } Err(e) => { - debug!("Skipping invalid cookie '{}': {}", name, e); + log::debug!("Skipping invalid cookie '{}': {}", name, e); } } } @@ -125,46 +124,17 @@ fn create_python_stream( generator: Py, ) -> Pin> + Send>> { Box::pin(futures::stream::unfold(generator, |generator| async move { - // Use spawn_blocking to execute the Python generator call in a separate thread - // This prevents blocking the async runtime when the generator contains blocking operations match tokio::task::spawn_blocking(move || { Python::with_gil(|py| { let gen = generator.bind(py); - // Check if this is an async generator first - let is_async_gen = gen.hasattr("__anext__").unwrap_or(false); - - if is_async_gen { - // For async generators, we expect them to be converted to sync generators in Python - debug!("Detected async generator - this should be handled in Python layer"); - None - } else { - // Try to get the next value from the generator (sync) - match gen.call_method0("__next__") { - Ok(value) => { - match value.extract::() { - Ok(string_value) => { - debug!("Generator yielded: {}", string_value); - Some((string_value, generator)) - } - Err(extract_err) => { - debug!( - "Failed to extract string from generator value: {}", - extract_err - ); - None // End of stream - } - } - } - Err(call_err) => { - // Check if this is a StopIteration (normal end) or actual error - if call_err.is_instance_of::(py) { - debug!("Generator exhausted (StopIteration)"); - } else { - debug!("Generator call error: {}", call_err); - } - None // End of stream + match gen.call_method0("__next__") { + Ok(value) => value.extract::().ok().map(|s| (s, generator)), + Err(e) => { + if !e.is_instance_of::(py) { + log::error!("Generator error: {}", e); } + None } } }) @@ -172,14 +142,7 @@ fn create_python_stream( .await { Ok(Some((string_value, generator))) => Some((Ok(Bytes::from(string_value)), generator)), - Ok(None) => None, - Err(join_err) => { - debug!( - "Failed to execute generator call in spawn_blocking: {}", - join_err - ); - None - } + _ => None, } })) } @@ -433,12 +396,10 @@ impl PyResponse { impl FromPyObject<'_, '_> for Response { type Error = PyErr; + #[inline] fn extract(obj: pyo3::Borrowed<'_, '_, PyAny>) -> Result { - // Only extract from actual Response objects, not StreamingResponse let type_name = obj.get_type().name()?; - debug!("Attempting to extract Response from type: {}", type_name); if type_name != "Response" { - debug!("Type mismatch: expected Response, got {}", type_name); return Err(PyErr::new::(format!( "Expected Response, got {}", type_name @@ -452,10 +413,6 @@ impl FromPyObject<'_, '_> for Response { let file_path: Option = obj.getattr("file_path")?.extract()?; let cookies: Cookies = obj.getattr("cookies")?.extract()?; - debug!( - "Successfully extracted Response with status {}", - status_code - ); Ok(Response { status_code, response_type, @@ -470,93 +427,22 @@ impl FromPyObject<'_, '_> for Response { impl FromPyObject<'_, '_> for StreamingResponse { type Error = PyErr; + #[inline] fn extract(obj: pyo3::Borrowed<'_, '_, PyAny>) -> Result { - // Check if it's a StreamingResponse by checking attributes rather than strict type name - let type_name = obj - .get_type() - .name() - .map(|n| n.to_string()) - .unwrap_or_else(|_| "unknown".to_string()); - debug!("=== STREAMING RESPONSE EXTRACTION START ==="); - debug!( - "Attempting to extract StreamingResponse from type: {}", - type_name - ); - - // Check if it has the required attributes for a StreamingResponse - let has_content = obj.hasattr("content").unwrap_or(false); - let has_status_code = obj.hasattr("status_code").unwrap_or(false); - let has_headers = obj.hasattr("headers").unwrap_or(false); - let has_media_type = obj.hasattr("media_type").unwrap_or(false); - - debug!( - "Attribute check: content={}, status_code={}, headers={}, media_type={}", - has_content, has_status_code, has_headers, has_media_type - ); - - // For StreamingResponse, we need content and media_type specifically - if !has_content || !has_status_code || !has_headers || !has_media_type { - debug!("Missing required StreamingResponse attributes"); - return Err(PyErr::new::(format!( - "Object is missing required StreamingResponse attributes" - ))); + if !obj.hasattr("content").unwrap_or(false) || !obj.hasattr("media_type").unwrap_or(false) { + return Err(PyErr::new::( + "Object is missing required StreamingResponse attributes", + )); } - debug!("All attributes present, proceeding with extraction"); - - let status_code: u16 = match obj.getattr("status_code") { - Ok(attr) => match attr.extract() { - Ok(code) => { - debug!("Successfully extracted status_code: {}", code); - code - } - Err(e) => { - debug!("Failed to extract status_code as u16: {}", e); - return Err(e); - } - }, - Err(e) => { - debug!("Failed to get status_code attribute: {}", e); - return Err(e); - } - }; - - let mut headers: Headers = match obj.getattr("headers") { - Ok(attr) => match attr.extract() { - Ok(headers) => { - debug!("Successfully extracted headers"); - headers - } - Err(e) => { - debug!("Failed to extract headers: {}", e); - return Err(e.into()); - } - }, - Err(e) => { - debug!("Failed to get headers attribute: {}", e); - return Err(e); - } - }; + let status_code: u16 = obj.getattr("status_code")?.extract()?; + let mut headers: Headers = obj.getattr("headers")?.extract()?; - // Ensure proper SSE headers are set if media_type is text/event-stream - let media_type: String = match obj.getattr("media_type") { - Ok(attr) => match attr.extract() { - Ok(media_type) => { - debug!("Successfully extracted media_type: {}", media_type); - media_type - } - Err(e) => { - debug!("Failed to extract media_type: {}", e); - "text/event-stream".to_string() - } - }, - Err(e) => { - debug!("Failed to get media_type attribute: {}", e); - "text/event-stream".to_string() - } - }; + let media_type: String = obj + .getattr("media_type") + .and_then(|a| a.extract()) + .unwrap_or_else(|_| "text/event-stream".to_string()); - // Set proper SSE headers if needed if media_type == "text/event-stream" { headers.set("Content-Type".to_string(), "text/event-stream".to_string()); if headers.get("Cache-Control".to_string()).is_none() { @@ -567,22 +453,8 @@ impl FromPyObject<'_, '_> for StreamingResponse { } } - let content: pyo3::Py = match obj.getattr("content") { - Ok(attr) => { - debug!("Successfully got content attribute"); - attr.unbind() - } - Err(e) => { - debug!("Failed to get content attribute: {}", e); - return Err(e); - } - }; + let content: pyo3::Py = obj.getattr("content")?.unbind(); - debug!("=== STREAMING RESPONSE EXTRACTION SUCCESS ==="); - debug!( - "Successfully extracted StreamingResponse with status {} from type {}", - status_code, type_name - ); Ok(StreamingResponse::new(status_code, headers, content)) } } diff --git a/src/websockets/mod.rs b/src/websockets/mod.rs index 89be7ddf3..ee1f8e45a 100644 --- a/src/websockets/mod.rs +++ b/src/websockets/mod.rs @@ -19,6 +19,8 @@ use std::sync::Arc; use tokio::sync::mpsc; use uuid::Uuid; +use crate::runtime; + use registry::{Register, WebSocketRegistry}; use std::collections::HashMap; @@ -190,7 +192,7 @@ impl WebSocketConnector { let recipient_id = Uuid::parse_str(&recipient_id).unwrap(); let sender_id = self.id; - let awaitable = pyo3_async_runtimes::tokio::future_into_py(py, async move { + let awaitable = runtime::future_into_py(py, async move { match registry.try_send(SendText { message, sender_id, @@ -220,7 +222,7 @@ impl WebSocketConnector { let registry = self.registry_addr.clone(); let sender_id = self.id; - let awaitable = pyo3_async_runtimes::tokio::future_into_py(py, async move { + let awaitable = runtime::future_into_py(py, async move { match registry.try_send(SendMessageToAll { message, sender_id }) { Ok(_) => println!("Message sent successfully"), Err(e) => println!("Failed to send message: {}", e), From 4c3be944961cced0970d31cc7522728e2134dd5a Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sat, 21 Mar 2026 01:32:23 +0000 Subject: [PATCH 049/106] Release 0.82.0 Made-with: Cursor --- Cargo.toml | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6b86bbd52..b1b84e9d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.81.0" +version = "0.82.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index f95105915..0af0fbaa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "robyn" -version = "0.81.0" +version = "0.82.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." readme = "README.md" authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }] @@ -69,7 +69,7 @@ test = [ [tool.poetry] name = "robyn" -version = "0.81.0" +version = "0.82.0" description = "A Super Fast Async Python Web Framework with a Rust runtime." authors = ["Sanskar Jethi "] From 2e70726310d9349073a2613d0104b6f5ecb1652b Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:55:52 +0000 Subject: [PATCH 050/106] feat: speed up CI with caching, reduced matrix, and path filters (#1338) * feat: speed up CI with caching, reduced matrix, and path filters - Add Swatinem/rust-cache to Python and Rust CI for Cargo caching - Add pip caching via actions/setup-python cache option - Add concurrency groups with cancel-in-progress to all workflows - Reduce PR matrix to ubuntu + Python 3.12 only (full 3x5 on push to main) - Add path filters so docs/unrelated changes don't trigger CI - Remove unnecessary universal2 cross-compilation from test builds Made-with: Cursor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: broaden lint-pr paths filter to cover all Python files The lint job runs ruff/isort against the repo root but previously only triggered on changes to robyn/, integration_tests/, and unit_tests/. Use **/*.py and pyproject.toml so changes to any Python file or the lint config always trigger the check. Made-with: Cursor * fix: harden WebSocket close protocol and UUID parsing - Replace Uuid::parse_str().unwrap() with proper error handling in sync_send_to and async_send_to, returning PyValueError instead of panicking on invalid UUIDs - Replace fragile "Connection closed" magic string with a dedicated CloseConnection actix message type, sending a proper WebSocket close frame via ctx.close(None) instead of relying on string comparison Made-with: Cursor * chore: update Cargo.lock Made-with: Cursor --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/lint-pr.yml | 7 +++++++ .github/workflows/python-CI.yml | 35 ++++++++++++++++++++++++++++++--- .github/workflows/rust-CI.yml | 23 ++++++++++++++++++++-- Cargo.lock | 2 +- noxfile.py | 8 -------- src/websockets/mod.rs | 26 ++++++++++++++++-------- src/websockets/registry.rs | 13 ++++++------ 7 files changed, 86 insertions(+), 28 deletions(-) diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 531ea277a..205f30fff 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -3,7 +3,14 @@ name: Lint PR on: pull_request: branches: [main] + paths: + - "**/*.py" + - "pyproject.toml" + - ".github/workflows/lint-pr.yml" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: UV_SYSTEM_PYTHON: 1 diff --git a/.github/workflows/python-CI.yml b/.github/workflows/python-CI.yml index 30f3745cc..96173853a 100644 --- a/.github/workflows/python-CI.yml +++ b/.github/workflows/python-CI.yml @@ -1,16 +1,43 @@ # CI to test Robyn on major Linux, MacOS and Windows -on: [push, pull_request] +on: + push: + branches: [main] + paths: + - "robyn/**" + - "src/**" + - "integration_tests/**" + - "unit_tests/**" + - "noxfile.py" + - "pyproject.toml" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/python-CI.yml" + pull_request: + paths: + - "robyn/**" + - "src/**" + - "integration_tests/**" + - "unit_tests/**" + - "noxfile.py" + - "pyproject.toml" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/python-CI.yml" name: Python Continuous integration +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: strategy: fail-fast: false matrix: - os: ["windows", "ubuntu", "macos"] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu"]') || fromJSON('["windows", "ubuntu", "macos"]') }} + python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.12"]') || fromJSON('["3.10", "3.11", "3.12", "3.13", "3.14"]') }} name: ${{ matrix.os }} tests with python ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest steps: @@ -19,6 +46,8 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + cache: "pip" + - uses: Swatinem/rust-cache@v2 - name: Set up Nox uses: wntrblm/nox@2024.03.02 with: diff --git a/.github/workflows/rust-CI.yml b/.github/workflows/rust-CI.yml index ed47ef278..7aa0b61c3 100644 --- a/.github/workflows/rust-CI.yml +++ b/.github/workflows/rust-CI.yml @@ -1,9 +1,26 @@ # CI to build, test, format, and lint the Rust code -on: [push, pull_request] +on: + push: + branches: [main] + paths: + - "src/**" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/rust-CI.yml" + pull_request: + paths: + - "src/**" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/rust-CI.yml" name: Rust Continuous integration +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: check: name: Check @@ -11,6 +28,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - run: cargo check test: @@ -19,6 +37,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - run: cargo test fmt: @@ -39,6 +58,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: clippy + - uses: Swatinem/rust-cache@v2 - run: cargo clippy # -- -D warnings - diff --git a/Cargo.lock b/Cargo.lock index af3e19479..4b9deb785 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,7 +1514,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.81.0" +version = "0.82.0" dependencies = [ "actix", "actix-files", diff --git a/noxfile.py b/noxfile.py index 040619d24..ab1c7e394 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,5 +1,3 @@ -import sys - import nox @@ -30,12 +28,6 @@ def tests(session): "dist", ] - if sys.platform == "darwin": - session.run("rustup", "target", "add", "x86_64-apple-darwin") - session.run("rustup", "target", "add", "aarch64-apple-darwin") - args.append("--target") - args.append("universal2-apple-darwin") - session.run(*args) session.run("pip", "install", "--no-index", "--find-links=dist/", "robyn") session.run("pytest") diff --git a/src/websockets/mod.rs b/src/websockets/mod.rs index ee1f8e45a..7c5a8aa29 100644 --- a/src/websockets/mod.rs +++ b/src/websockets/mod.rs @@ -3,7 +3,7 @@ pub mod registry; use crate::executors::web_socket_executors::execute_ws_function; use crate::types::function_info::FunctionInfo; use crate::types::multimap::QueryParams; -use registry::{Close, SendMessageToAll, SendText}; +use registry::{Close, CloseConnection, SendMessageToAll, SendText}; use actix::prelude::*; use actix::{Actor, AsyncContext, StreamHandler}; @@ -129,14 +129,19 @@ impl Handler for WebSocketConnector { fn handle(&mut self, msg: SendText, ctx: &mut Self::Context) { if self.id == msg.recipient_id { ctx.text(msg.message.clone()); - if msg.message == "Connection closed" { - // Close the WebSocket connection - ctx.stop(); - } } } } +impl Handler for WebSocketConnector { + type Result = (); + + fn handle(&mut self, _msg: CloseConnection, ctx: &mut Self::Context) { + ctx.close(None); + ctx.stop(); + } +} + /// Handler for ws::Message message impl StreamHandler> for WebSocketConnector { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { @@ -169,8 +174,10 @@ impl StreamHandler> for WebSocketConnecto #[pymethods] impl WebSocketConnector { - pub fn sync_send_to(&self, recipient_id: String, message: String) { - let recipient_id = Uuid::parse_str(&recipient_id).unwrap(); + pub fn sync_send_to(&self, recipient_id: String, message: String) -> PyResult<()> { + let recipient_id = Uuid::parse_str(&recipient_id).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid recipient_id UUID: {e}")) + })?; match self.registry_addr.try_send(SendText { message, @@ -180,6 +187,7 @@ impl WebSocketConnector { Ok(_) => println!("Message sent successfully"), Err(e) => println!("Failed to send message: {}", e), } + Ok(()) } pub fn async_send_to( @@ -189,7 +197,9 @@ impl WebSocketConnector { message: String, ) -> PyResult> { let registry = self.registry_addr.clone(); - let recipient_id = Uuid::parse_str(&recipient_id).unwrap(); + let recipient_id = Uuid::parse_str(&recipient_id).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid recipient_id UUID: {e}")) + })?; let sender_id = self.id; let awaitable = runtime::future_into_py(py, async move { diff --git a/src/websockets/registry.rs b/src/websockets/registry.rs index 702a50ac1..f4d619691 100644 --- a/src/websockets/registry.rs +++ b/src/websockets/registry.rs @@ -107,17 +107,18 @@ impl Message for Close { type Result = (); } +pub struct CloseConnection; + +impl Message for CloseConnection { + type Result = (); +} + impl Handler for WebSocketRegistry { type Result = (); fn handle(&mut self, msg: Close, _ctx: &mut Self::Context) { if let Some(client) = self.clients.remove(&msg.id) { - // Send a close message to the client before removing it - client.do_send(SendText { - recipient_id: msg.id, - message: "Connection closed".to_string(), - sender_id: msg.id, - }); + client.do_send(CloseConnection); } } } From 41aafcfa9f2bfe95d5f83050b7bd4b4881efce51 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 21 Mar 2026 20:29:07 +0000 Subject: [PATCH 051/106] feat(website): comprehensive SEO overhaul and blog scaffold (#1340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support binary WebSocket frames in message handlers Closes #1332 Propagate binary WebSocket frames (opcode 0x2) through the entire Rust→Python pipeline instead of silently echoing them back. - Add WsPayload enum (Text/Binary) to replace String throughout the WebSocket channel, registry messages, and send methods - Forward ws::Message::Binary through the channel so Python handlers receive bytes for binary frames and str for text frames - Accept str|bytes in send/broadcast methods, emitting the correct frame type on the wire - Add receive() returning str|bytes, fix receive_text/receive_bytes to validate frame type Made-with: Cursor * fix: harden WebSocket API safety and update type stubs - Replace UUID unwrap() with proper error handling in sync_send_to and async_send_to, returning PyValueError instead of panicking - Update robyn.pyi type stubs to accept str | bytes for send/broadcast methods, matching the Rust implementations - Add warning log in extract_ws_return for unsupported handler return types to aid debugging Made-with: Cursor * fix: add Python 3.14 to linux-cross matrix and bump maturin - Add cp314-cp314 entry to the linux-cross build matrix - Bump pinned maturin from v1.12.0 to v1.12.6 to support Python 3.14's build-details.json format (missing extension_suffix field) Made-with: Cursor * docs: update WebSocket docs for binary frame support - Document new receive() and send() methods for mixed text/binary frames - Add binary and mixed-frame code examples to both EN and ZH docs - Update API reference tables with receive(), send(), and updated descriptions for receive_text/bytes, send_bytes, and broadcast - Note that receive_text/receive_bytes now raise TypeError on wrong frame type Made-with: Cursor * feat(website): comprehensive SEO overhaul and blog scaffold Addresses critical SEO gaps across the Robyn website: - Add robots.txt and auto-generated sitemap via next-sitemap - Create reusable SEO component with canonical URLs, hreflang (en/zh), Open Graph, Twitter Cards, and JSON-LD structured data - Fix placeholder @yourTwitterHandle → @robaborobyn - Add per-page meta tags to community, releases, and docs pages - Add JSON-LD schemas: WebSite, SoftwareSourceCode, Organization, BreadcrumbList, TechArticle - Fix hardcoded lang="en" → dynamic locale from Next.js - Fix empty alt attributes on logo, testimonial photos, release images - Replace raw tags with Next.js for internal navigation - Fix heading hierarchy (community h2→h1) - Fix "wor oad" typo in testimonial - Add custom 404 page - Add site.webmanifest with proper icon declarations - Scaffold blog section with index, [slug] dynamic routes, and sample welcome post - Add Blog link to header and footer navigation - Rename package.json from template name to robyn-website Made-with: Cursor * fix: address PR review findings across website and websocket code Website/SEO fixes: - Fix hreflang duplication: use router.locales/defaultLocale and strip locale prefix from path before building alternates - Escape JSON-LD output to prevent script-breakout via frontmatter - Remove references to non-existent favicon PNGs, use existing robynog.png - Add aria-current="page" to active breadcrumb item in blog posts - Guard getStaticProps/getBlogPostBySlug against missing files (return notFound: true) - Use siteUrl variable in next-sitemap alternateRefs instead of hardcoded URL - Strip query strings and hash fragments in breadcrumb path builder - Wrap fetchStars in useCallback and add to useEffect dependency array - Use dynamic locale for docs link in 404 page - Extract shared formatDate utility to deduplicate blog components Rust/Python fixes: - Rename sender_id → recipient_id in robyn.pyi stubs to match Rust bindings - Remove repr() logging from extract_ws_return to prevent data leakage - Add WsPayload::Close variant to replace "Connection closed" magic string - Switch WebSocket message channel from unbounded to bounded (cap 256) - Log warning when bounded channel is full/closed instead of silently dropping - Propagate registry try_send errors to Python as exceptions instead of swallowing with Ok(()) Made-with: Cursor * fix: normalize SITE_URL, UTC dates, websocket channel lifecycle - Trim trailing slashes from SITE_URL to prevent double-slash URLs - Add timeZone: 'UTC' to formatDate to prevent off-by-one display for negative UTC offsets - Stop cloning message_sender into Python-facing WebSocketConnector clones so the channel properly closes when the actor stops - Send terminal None through channel before dropping sender in stopped() so receive() returns None on disconnect - Consume SendMessage payload by value instead of borrowing and cloning to avoid unnecessary allocations Made-with: Cursor * fix: remove WebSocket binary frame changes from docs-only branch Reverts WebSocket code and doc changes that were accidentally included in the SEO overhaul branch. These belong on feat/binary-websocket-frames. Made-with: Cursor * fix: revert release-CI.yml changes from docs-only branch Made-with: Cursor --- docs_src/next-sitemap.config.js | 20 ++ docs_src/package-lock.json | 201 +++++++++++++++++- docs_src/package.json | 6 +- docs_src/public/robots.txt | 5 + docs_src/public/site.webmanifest | 16 ++ docs_src/src/components/Footer.jsx | 1 + docs_src/src/components/Header.jsx | 24 ++- docs_src/src/components/SEO.jsx | 157 ++++++++++++++ docs_src/src/components/Testimonials.jsx | 4 +- .../src/components/documentation/Layout.jsx | 35 ++- docs_src/src/components/releases/mdx.jsx | 2 +- docs_src/src/content/blog/welcome.mdx | 28 +++ docs_src/src/lib/formatDate.js | 6 +- docs_src/src/lib/getAllBlogPosts.js | 52 +++++ docs_src/src/pages/404.jsx | 40 ++++ docs_src/src/pages/_document.jsx | 16 +- docs_src/src/pages/blog/[slug].jsx | 126 +++++++++++ docs_src/src/pages/blog/index.jsx | 72 +++++++ docs_src/src/pages/community.jsx | 28 ++- docs_src/src/pages/index.jsx | 92 +++----- docs_src/src/pages/releases/index.jsx | 6 + src/websockets/mod.rs | 26 +-- src/websockets/registry.rs | 13 +- 23 files changed, 835 insertions(+), 141 deletions(-) create mode 100644 docs_src/next-sitemap.config.js create mode 100644 docs_src/public/robots.txt create mode 100644 docs_src/public/site.webmanifest create mode 100644 docs_src/src/components/SEO.jsx create mode 100644 docs_src/src/content/blog/welcome.mdx create mode 100644 docs_src/src/lib/getAllBlogPosts.js create mode 100644 docs_src/src/pages/404.jsx create mode 100644 docs_src/src/pages/blog/[slug].jsx create mode 100644 docs_src/src/pages/blog/index.jsx diff --git a/docs_src/next-sitemap.config.js b/docs_src/next-sitemap.config.js new file mode 100644 index 000000000..9f37b1e52 --- /dev/null +++ b/docs_src/next-sitemap.config.js @@ -0,0 +1,20 @@ +/** @type {import('next-sitemap').IConfig} */ +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://robyn.tech' + +module.exports = { + siteUrl, + generateRobotsTxt: false, + generateIndexSitemap: false, + outDir: 'public', + exclude: ['/api/*'], + alternateRefs: [ + { + href: siteUrl, + hreflang: 'en', + }, + { + href: `${siteUrl}/zh`, + hreflang: 'zh', + }, + ], +} diff --git a/docs_src/package-lock.json b/docs_src/package-lock.json index fa0f77ea7..fbe750221 100644 --- a/docs_src/package-lock.json +++ b/docs_src/package-lock.json @@ -1,11 +1,11 @@ { - "name": "tailwindui-template", + "name": "robyn-website", "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "tailwindui-template", + "name": "robyn-website", "version": "0.1.0", "dependencies": { "@algolia/autocomplete-core": "^1.9.3", @@ -27,12 +27,14 @@ "feed": "^4.2.2", "focus-visible": "^5.2.0", "framer-motion": "^10.12.16", + "gray-matter": "^4.0.3", "highlight.js": "^11.8.0", "mdx-annotations": "^0.1.3", "meilisearch": "^0.33.0", "next": "13.4.2", "next-mdx-remote": "^6.0.0", "next-router-mock": "^0.9.3", + "next-sitemap": "^4.2.3", "postcss-focus-visible": "^6.0.4", "prism-themes": "^1.9.0", "prismjs": "^1.29.0", @@ -260,6 +262,11 @@ "node": ">=6.9.0" } }, + "node_modules/@corex/deepmerge": { + "version": "4.0.43", + "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.43.tgz", + "integrity": "sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==" + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -3862,6 +3869,40 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -5448,6 +5489,14 @@ "json-buffer": "3.0.0" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -7222,9 +7271,12 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/mri": { "version": "1.2.0", @@ -8509,6 +8561,37 @@ "react": ">=17.0.0" } }, + "node_modules/next-sitemap": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz", + "integrity": "sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==", + "funding": [ + { + "url": "https://github.com/iamvishnusankar/next-sitemap.git" + } + ], + "dependencies": { + "@corex/deepmerge": "^4.0.43", + "@next/env": "^13.4.3", + "fast-glob": "^3.2.12", + "minimist": "^1.2.8" + }, + "bin": { + "next-sitemap": "bin/next-sitemap.mjs", + "next-sitemap-cjs": "bin/next-sitemap.cjs" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "next": "*" + } + }, + "node_modules/next-sitemap/node_modules/@next/env": { + "version": "13.5.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.11.tgz", + "integrity": "sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==" + }, "node_modules/node-fetch": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", @@ -11105,6 +11188,18 @@ "integrity": "sha512-bkWW9nIHOFkLwjQ1xqVaMbjjO5vhP26ERsH9Y3pKr8imthofEFIxlnOabkmGcw6ksRj9jWidcI65vvjJH/nTGg==", "peer": true }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -11289,6 +11384,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -11395,6 +11495,14 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -13032,6 +13140,11 @@ "regenerator-runtime": "^0.13.4" } }, + "@corex/deepmerge": { + "version": "4.0.43", + "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.43.tgz", + "integrity": "sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==" + }, "@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -15700,6 +15813,36 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "requires": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -16742,6 +16885,11 @@ "json-buffer": "3.0.0" } }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, "kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -17965,9 +18113,9 @@ } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mri": { "version": "1.2.0", @@ -18802,6 +18950,24 @@ "integrity": "sha512-jl8eFe71LpMVGeBMpoxILkGfEgGY7IfLy8XPyv05/o61p5oQRNpoMmk46VMxRIpt0fI8XcvznBZKpDK6vbYQcQ==", "requires": {} }, + "next-sitemap": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz", + "integrity": "sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==", + "requires": { + "@corex/deepmerge": "^4.0.43", + "@next/env": "^13.4.3", + "fast-glob": "^3.2.12", + "minimist": "^1.2.8" + }, + "dependencies": { + "@next/env": { + "version": "13.5.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.11.tgz", + "integrity": "sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==" + } + } + }, "node-fetch": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", @@ -20515,6 +20681,15 @@ "integrity": "sha512-bkWW9nIHOFkLwjQ1xqVaMbjjO5vhP26ERsH9Y3pKr8imthofEFIxlnOabkmGcw6ksRj9jWidcI65vvjJH/nTGg==", "peer": true }, + "section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "requires": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + } + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -20657,6 +20832,11 @@ "resolved": "https://registry.npmjs.org/split-lines/-/split-lines-2.1.0.tgz", "integrity": "sha512-8dv+1zKgTpfTkOy8XZLFyWrfxO0NV/bj/3EaQ+hBrBxGv2DwiroljPjU8NlCr+59nLnsVm9WYT7lXKwe4TC6bw==" }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, "streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -20740,6 +20920,11 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==" + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", diff --git a/docs_src/package.json b/docs_src/package.json index fcbd3c3f4..f0eaaf8db 100644 --- a/docs_src/package.json +++ b/docs_src/package.json @@ -1,10 +1,10 @@ { - "name": "tailwindui-template", + "name": "robyn-website", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build && next-sitemap", "start": "next start", "lint": "next lint" }, @@ -29,12 +29,14 @@ "feed": "^4.2.2", "focus-visible": "^5.2.0", "framer-motion": "^10.12.16", + "gray-matter": "^4.0.3", "highlight.js": "^11.8.0", "mdx-annotations": "^0.1.3", "meilisearch": "^0.33.0", "next": "13.4.2", "next-mdx-remote": "^6.0.0", "next-router-mock": "^0.9.3", + "next-sitemap": "^4.2.3", "postcss-focus-visible": "^6.0.4", "prism-themes": "^1.9.0", "prismjs": "^1.29.0", diff --git a/docs_src/public/robots.txt b/docs_src/public/robots.txt new file mode 100644 index 000000000..336d46c88 --- /dev/null +++ b/docs_src/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Disallow: /api/ + +Sitemap: https://robyn.tech/sitemap.xml diff --git a/docs_src/public/site.webmanifest b/docs_src/public/site.webmanifest new file mode 100644 index 000000000..2227748a7 --- /dev/null +++ b/docs_src/public/site.webmanifest @@ -0,0 +1,16 @@ +{ + "name": "Robyn Framework", + "short_name": "Robyn", + "description": "A fast, innovator-friendly, and community-driven Python web framework powered by Rust", + "start_url": "/", + "display": "standalone", + "background_color": "#000000", + "theme_color": "#000000", + "icons": [ + { + "src": "/robynog.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/docs_src/src/components/Footer.jsx b/docs_src/src/components/Footer.jsx index 54790e08f..544b9ab75 100644 --- a/docs_src/src/components/Footer.jsx +++ b/docs_src/src/components/Footer.jsx @@ -48,6 +48,7 @@ export function Footer() { Home Documentation Releases + Blog Community Discord diff --git a/docs_src/src/components/Header.jsx b/docs_src/src/components/Header.jsx index 3198b7c41..223d6a883 100644 --- a/docs_src/src/components/Header.jsx +++ b/docs_src/src/components/Header.jsx @@ -1,4 +1,4 @@ -import { Fragment, useEffect, useRef, useState } from 'react' +import { Fragment, useCallback, useEffect, useRef, useState } from 'react' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' @@ -92,6 +92,7 @@ function MobileNavigation(props) { Documentation Releases + Blog Community GitHub @@ -133,6 +134,7 @@ function DesktopNavigation(props) {