From 66a7c7bc1835d052cb09ea801c1dc517e76124ac Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 26 Jan 2026 14:22:19 -0600 Subject: [PATCH 1/3] Scaffold vitepress site --- .github/workflows/deploy-docs.yml | 72 + docs/.gitignore | 3 + docs/.vitepress/config.mts | 63 + docs/adapter-contract.md | 132 -- docs/guide/adapters/axum.md | 232 +++ docs/guide/adapters/cloudflare.md | 248 +++ docs/guide/adapters/fastly.md | 224 +++ docs/guide/adapters/overview.md | 105 ++ docs/guide/architecture.md | 134 ++ docs/guide/cli-reference.md | 247 +++ docs/guide/configuration.md | 319 ++++ docs/guide/getting-started.md | 119 ++ docs/guide/handlers.md | 243 +++ docs/guide/middleware.md | 210 +++ docs/guide/proxying.md | 244 +++ docs/guide/routing.md | 163 ++ docs/guide/streaming.md | 132 ++ docs/guide/what-is-edgezero.md | 47 + docs/index.md | 29 + docs/manifest.md | 197 --- docs/package-lock.json | 2514 +++++++++++++++++++++++++++++ docs/package.json | 15 + 22 files changed, 5363 insertions(+), 329 deletions(-) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 docs/.gitignore create mode 100644 docs/.vitepress/config.mts delete mode 100644 docs/adapter-contract.md create mode 100644 docs/guide/adapters/axum.md create mode 100644 docs/guide/adapters/cloudflare.md create mode 100644 docs/guide/adapters/fastly.md create mode 100644 docs/guide/adapters/overview.md create mode 100644 docs/guide/architecture.md create mode 100644 docs/guide/cli-reference.md create mode 100644 docs/guide/configuration.md create mode 100644 docs/guide/getting-started.md create mode 100644 docs/guide/handlers.md create mode 100644 docs/guide/middleware.md create mode 100644 docs/guide/proxying.md create mode 100644 docs/guide/routing.md create mode 100644 docs/guide/streaming.md create mode 100644 docs/guide/what-is-edgezero.md create mode 100644 docs/index.md delete mode 100644 docs/manifest.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..68ceed5 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,72 @@ +name: Deploy VitePress Docs + +on: + push: + branches: [main] + paths: + - 'docs/**' + - '.github/workflows/deploy-docs.yml' + workflow_dispatch: # Allow manual triggers + +# Sets permissions for GitHub Pages deployment +permissions: + contents: read + pages: write + id-token: write + +# Prevent concurrent deployments +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # For lastUpdated feature + + - name: Retrieve Node.js version + id: node-version + run: | + if [ -f .tool-versions ]; then + echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + else + echo "node-version=20" >> $GITHUB_OUTPUT + fi + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ steps.node-version.outputs.node-version }} + cache: 'npm' + cache-dependency-path: docs/package-lock.json + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Install dependencies + working-directory: docs + run: npm ci + + - name: Build with VitePress + working-directory: docs + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..57a09c3 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules +.vitepress/dist +.vitepress/cache diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..8f4fb25 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,63 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "EdgeZero", + description: "Production-ready toolkit for portable edge HTTP workloads", + base: "/edgezero/", + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide/getting-started' }, + { text: 'Adapters', link: '/guide/adapters/overview' }, + { text: 'Reference', link: '/guide/configuration' }, + ], + + sidebar: [ + { + text: 'Introduction', + items: [ + { text: 'What is EdgeZero?', link: '/guide/what-is-edgezero' }, + { text: 'Getting Started', link: '/guide/getting-started' }, + { text: 'Architecture', link: '/guide/architecture' } + ] + }, + { + text: 'Core Concepts', + items: [ + { text: 'Routing', link: '/guide/routing' }, + { text: 'Handlers & Extractors', link: '/guide/handlers' }, + { text: 'Middleware', link: '/guide/middleware' }, + { text: 'Streaming', link: '/guide/streaming' }, + { text: 'Proxying', link: '/guide/proxying' } + ] + }, + { + text: 'Adapters', + items: [ + { text: 'Overview', link: '/guide/adapters/overview' }, + { text: 'Fastly Compute', link: '/guide/adapters/fastly' }, + { text: 'Cloudflare Workers', link: '/guide/adapters/cloudflare' }, + { text: 'Axum (Native)', link: '/guide/adapters/axum' } + ] + }, + { + text: 'Reference', + items: [ + { text: 'Configuration (edgezero.toml)', link: '/guide/configuration' }, + { text: 'CLI Reference', link: '/guide/cli-reference' } + ] + } + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/stackpop/edgezero' } + ], + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright 2024-present Stackpop' + } + } +}) diff --git a/docs/adapter-contract.md b/docs/adapter-contract.md deleted file mode 100644 index da35df7..0000000 --- a/docs/adapter-contract.md +++ /dev/null @@ -1,132 +0,0 @@ -# Provider Adapter Contract - -This document defines the expectations EdgeZero places on provider adapters. The -current implementations (Fastly Compute@Edge and Cloudflare Workers) satisfy -these rules; new targets should follow the same contract so that the shared -`edgezero-core` services behave consistently. - -## Goals - -Adapters translate provider-specific HTTP primitives into the portable `App` -in `edgezero-core`. They must preserve request semantics, stream responses -without buffering, expose provider context, and offer a proxy bridge so handlers -can forward traffic without knowing which platform they are on. - -## Request conversion - -Each adapter exposes an `into_core_request` helper that accepts the provider's -request type and returns `edgezero_core::http::Request`. The conversion must: - -- Preserve the HTTP method exactly (`GET`, `POST`, etc.). -- Parse the full URI (path and query string) into an `http::Uri`. Reject invalid - URIs with `EdgeError::bad_request`. -- Copy all headers into the core request. Provider-specific headers may be - filtered only when they clash with the platform defaults (e.g. Fastly's host - override). -- Consume the request body into an `edgezero_core::body::Body`. If the provider offers - a streaming API, it should be exposed via `Body::Stream`; otherwise a single - buffered chunk is acceptable. -- Insert a provider context struct (e.g. `FastlyRequestContext`) into the request - extensions. The context should expose metadata such as client IP addresses or - environment handles so handlers can reach platform APIs. - -## Response conversion - -Adapters also expose `from_core_response` (or equivalent) to transform an -`edgezero_core::http::Response` into the provider response type. Implementations must: - -- Map HTTP status codes verbatim. -- Copy headers, respecting casing rules enforced by the provider. -- Preserve streaming bodies. `Body::Stream` should be written chunk-by-chunk to - the provider output without buffering the entire payload. -- Handle encoding helpers (`decode_gzip_stream`, `decode_brotli_stream`) where a - provider requires transparent decompression. - -## Dispatch helper - -Adapters surface a `dispatch` function that bridges from the provider event loop -into the shared router (`App::router().oneshot(...)`). It should: - -1. Convert the incoming provider request with `into_core_request`. -2. Await the router future. -3. Convert the resulting `Response` back into the provider type. -4. Map any `EdgeError` into the provider's error type so failures surface as - HTTP 5xx responses instead of panicking. - -This helper is what demo entrypoints and adapters call when wiring their -platform-specific main functions. - -## Proxy integration - -Adapters implement `edgezero_core::proxy::ProxyClient` so handlers can forward outbound -requests. The client must: - -- Accept a `ProxyRequest` created with `ProxyRequest::from_request`. -- Build and send an outbound provider request, reusing headers and streaming the - body without buffering. -- Convert the provider response into a `ProxyResponse`, again preserving - streaming behaviour and normalising encodings. -- Attach a diagnostic header (e.g. `x-edgezero-proxy`) identifying which adapter - forwarded the call. -- Surface provider errors as `EdgeError::internal` so applications can decide - how to respond. - -## Logging initialisation - -Each adapter exports an `init_logger` helper for platform-specific logging -backends (`log_fastly` or `console_log!`). Applications should call it before -building the router. New adapters should provide a comparable helper so apps -consistently opt into logging. - -## Contract tests - -To keep the contract enforceable, each adapter includes integration tests that -validate request/response conversions and the dispatch helper. Fastly and -Cloudflare now ship `tests/contract.rs` suites that exercise: - -- `into_core_request` for method, URI, header, body, and context propagation. -- `from_core_response` for status propagation and streamed body writes. -- `dispatch` for routed handlers, body passthrough, and streaming responses. - -Because the Fastly SDK links against the Compute@Edge host functions, the -contract tests compile only for `wasm32-wasip1`. Run them with: - -```bash -rustup target add wasm32-wasip1 # once per workstation -cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --tests -``` - -Provide a Wasm runner (Wasmtime or Viceroy) via -`CARGO_TARGET_WASM32_WASIP1_RUNNER` if you want to execute the binaries instead -of running `--no-run`. - -Cloudflare's adapter relies on `wasm32-unknown-unknown`. The contract suite lives -in `crates/edgezero-adapter-cloudflare/tests/contract.rs` and uses -`wasm-bindgen-test` to run under the Workers runtime shims. Execute it with: - -```bash -rustup target add wasm32-unknown-unknown # once per workstation -cargo test -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown --tests -``` - -Configure a wasm-bindgen test runner via `wasm-bindgen-test-runner` (Node.js) or -`wasm-pack` depending on your tooling; the suite asserts the same request, -response, and streaming guarantees as the Fastly tests. - -## Onboarding new adapters - -When bringing up another adapter: - -1. Implement request/response conversion functions that follow the rules above. -2. Provide a context type exposing the adapter's metadata and insert it in - `into_core_request`. -3. Implement a `dispatch` wrapper plus logging helper. -4. Wire up a `ProxyClient` that streams bodies and normalises encodings. -5. Copy the contract test suite, swapping in the new adapter types. Ensure the - tests are gated to the target architecture if the adapter SDK does not - compile for native hosts. -6. Register the adapter with `edgezero-adapter::register_adapter` (typically in a - `cli` module using the `ctor` crate) so the CLI can discover it dynamically. - -Adapters that fulfil these steps can be dropped into the EdgeZero CLI without -requiring changes to application code. diff --git a/docs/guide/adapters/axum.md b/docs/guide/adapters/axum.md new file mode 100644 index 0000000..b1ac617 --- /dev/null +++ b/docs/guide/adapters/axum.md @@ -0,0 +1,232 @@ +# Axum (Native) + +Run EdgeZero applications natively using Axum and Tokio for local development, testing, and container deployments. + +## Overview + +The Axum adapter provides: + +- **Local development server** - Fast iteration without Wasm compilation +- **Native testing** - Run tests with standard `cargo test` +- **Container deployments** - Deploy to any platform supporting native binaries + +## Project Setup + +When scaffolding with `edgezero new my-app --adapters axum`, you get: + +``` +crates/my-app-adapter-axum/ +├── Cargo.toml +├── axum.toml +└── src/ + └── main.rs +``` + +### Entrypoint + +The Axum entrypoint wires the adapter: + +```rust +use edgezero_adapter_axum::AxumDevServer; +use my_app_core::App; + +#[tokio::main] +async fn main() { + // Initialize standard logging + env_logger::init(); + + let app = App::build(); + + AxumDevServer::new(app) + .bind("127.0.0.1:8787") + .run() + .await + .unwrap(); +} +``` + +## Development Server + +The `edgezero dev` command uses the Axum adapter: + +```bash +edgezero dev +``` + +This starts a server at `http://127.0.0.1:8787` with: + +- Hot reload support (via cargo watch integration) +- Standard logging to stdout +- Full handler debugging + +### Manual Start + +Run the Axum entrypoint directly: + +```bash +# Using the CLI +edgezero serve --adapter axum + +# Or directly with cargo +cargo run -p my-app-adapter-axum +``` + +## Building + +Build a native release binary: + +```bash +# Using the CLI +edgezero build --adapter axum + +# Or directly +cargo build -p my-app-adapter-axum --release +``` + +The binary is placed in `target/release/my-app-adapter-axum`. + +## Proxy Client + +The Axum adapter provides a native HTTP client for proxying: + +```rust +use edgezero_adapter_axum::AxumProxyClient; + +let client = AxumProxyClient::new(); +let response = ProxyService::new(client).forward(request).await?; +``` + +This uses `reqwest` under the hood for outbound HTTP requests. + +## Logging + +Use any standard Rust logging implementation: + +```rust +use log::{info, error}; + +#[tokio::main] +async fn main() { + // Simple logger + env_logger::init(); + + // Or use tracing + // tracing_subscriber::fmt::init(); + + info!("Starting server..."); +} +``` + +Configure log levels via environment variable: + +```bash +RUST_LOG=info edgezero dev +RUST_LOG=my_app=debug,edgezero_core=info edgezero dev +``` + +## Testing + +The Axum adapter enables standard Rust testing: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use edgezero_core::http::{Request, Method}; + + #[tokio::test] + async fn test_handler() { + let app = App::build(); + let router = app.router(); + + let request = Request::builder() + .method(Method::GET) + .uri("/hello") + .body(Body::empty()) + .unwrap(); + + let response = router.oneshot(request).await.unwrap(); + assert_eq!(response.status(), 200); + } +} +``` + +Run tests: + +```bash +cargo test -p my-app-core +cargo test -p my-app-adapter-axum +``` + +## Container Deployment + +Build and deploy as a standard container: + +```dockerfile +FROM rust:1.75 as builder +WORKDIR /app +COPY . . +RUN cargo build -p my-app-adapter-axum --release + +FROM debian:bookworm-slim +COPY --from=builder /app/target/release/my-app-adapter-axum /usr/local/bin/ +EXPOSE 8787 +CMD ["my-app-adapter-axum"] +``` + +## Configuration + +Configure the dev server in `axum.toml`: + +```toml +[server] +host = "127.0.0.1" +port = 8787 + +[logging] +level = "info" +``` + +Or via `edgezero.toml`: + +```toml +[adapters.axum.adapter] +crate = "crates/my-app-adapter-axum" +manifest = "crates/my-app-adapter-axum/axum.toml" + +[adapters.axum.commands] +build = "cargo build --release -p my-app-adapter-axum" +serve = "cargo run -p my-app-adapter-axum" +``` + +## Development Workflow + +A typical development workflow: + +1. **Start dev server**: `edgezero dev` +2. **Make changes** to handlers in `my-app-core` +3. **Test locally** with curl or browser +4. **Run tests**: `cargo test` +5. **Build for edge**: `edgezero build --adapter fastly` +6. **Deploy**: `edgezero deploy --adapter fastly` + +## Differences from Edge Adapters + +| Aspect | Axum | Fastly/Cloudflare | +|--------|------|-------------------| +| Compilation | Native | Wasm | +| Cold start | ~0ms | ~0ms (Wasm) | +| Memory | Unlimited | 128MB typical | +| Filesystem | Full access | Sandboxed | +| Network | Direct | Backend/fetch | +| Concurrency | Multi-threaded | Single-threaded | + +::: tip Development Parity +While Axum provides a convenient development environment, always test on actual edge platforms before deploying. Some edge-specific features (KV stores, geolocation) aren't available in the Axum adapter. +::: + +## Next Steps + +- Deploy to [Fastly Compute](/guide/adapters/fastly) for production +- Deploy to [Cloudflare Workers](/guide/adapters/cloudflare) as an alternative +- Explore [Configuration](/guide/configuration) for manifest options diff --git a/docs/guide/adapters/cloudflare.md b/docs/guide/adapters/cloudflare.md new file mode 100644 index 0000000..f757d23 --- /dev/null +++ b/docs/guide/adapters/cloudflare.md @@ -0,0 +1,248 @@ +# Cloudflare Workers + +Deploy EdgeZero applications to Cloudflare Workers using WebAssembly. + +## Prerequisites + +- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) +- Rust `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` + +## Project Setup + +When scaffolding with `edgezero new my-app --adapters cloudflare`, you get: + +``` +crates/my-app-adapter-cloudflare/ +├── Cargo.toml +├── wrangler.toml +└── src/ + └── main.rs +``` + +### wrangler.toml + +The Wrangler manifest configures your Worker: + +```toml +name = "my-app" +main = "build/worker/shim.mjs" +compatibility_date = "2024-01-01" + +[build] +command = "cargo build --release --target wasm32-unknown-unknown" +``` + +### Entrypoint + +The Cloudflare entrypoint wires the adapter: + +```rust +use edgezero_adapter_cloudflare::{dispatch, init_logger}; +use my_app_core::App; +use worker::*; + +#[event(fetch)] +async fn main(req: Request, env: Env, _ctx: Context) -> Result { + init_logger(); + let app = App::build(); + dispatch(&app, req).await +} +``` + +## Building + +Build for Cloudflare's Wasm target: + +```bash +# Using the CLI +edgezero build --adapter cloudflare + +# Or directly with cargo +cargo build -p my-app-adapter-cloudflare --target wasm32-unknown-unknown --release +``` + +## Local Development + +Run locally with Wrangler: + +```bash +# Using the CLI +edgezero serve --adapter cloudflare + +# Or directly +wrangler dev --config crates/my-app-adapter-cloudflare/wrangler.toml +``` + +This starts a local server at `http://127.0.0.1:8787`. + +## Deployment + +Deploy to Cloudflare Workers: + +```bash +# Using the CLI +edgezero deploy --adapter cloudflare + +# Or directly +wrangler publish --config crates/my-app-adapter-cloudflare/wrangler.toml +``` + +## Fetch API + +Cloudflare Workers use the global `fetch` API for outbound requests: + +```rust +use edgezero_adapter_cloudflare::CloudflareProxyClient; + +let client = CloudflareProxyClient::new(); +let response = ProxyService::new(client).forward(request).await?; +``` + +Unlike Fastly, there's no backend configuration needed - Workers can fetch any URL directly. + +## Logging + +Cloudflare Workers log via `console.log`. Initialize the logger: + +```rust +use edgezero_adapter_cloudflare::init_logger; + +fn main() { + init_logger(); +} +``` + +Configure logging level in `edgezero.toml`: + +```toml +[adapters.cloudflare.logging] +level = "info" +``` + +View logs in the Wrangler output or Cloudflare dashboard. + +## Context Access + +Access Cloudflare-specific APIs via the request context: + +```rust +use edgezero_adapter_cloudflare::CloudflareRequestContext; + +#[action] +async fn handler(RequestContext(ctx): RequestContext) -> Response { + if let Some(cf_ctx) = ctx.extensions().get::() { + // Access Cloudflare-specific data + let cf = cf_ctx.cf(); + // ... + } + + // ... +} +``` + +## Environment Variables & Secrets + +Define variables in `wrangler.toml`: + +```toml +[vars] +API_URL = "https://api.example.com" + +# Secrets are set via wrangler CLI +# wrangler secret put API_KEY +``` + +Access in handlers via the Cloudflare context or environment bindings. + +## KV Storage + +Use Cloudflare KV for edge storage: + +```toml +# wrangler.toml +[[kv_namespaces]] +binding = "MY_KV" +id = "abc123" +``` + +Access via the Cloudflare environment bindings in your handler. + +## Durable Objects + +For stateful edge computing, configure Durable Objects: + +```toml +# wrangler.toml +[durable_objects] +bindings = [ + { name = "COUNTER", class_name = "Counter" } +] +``` + +## Streaming + +Cloudflare Workers support streaming via `ReadableStream`: + +```rust +#[action] +async fn stream() -> Response { + let stream = async_stream::stream! { + for i in 0..100 { + yield Ok::<_, std::io::Error>(format!("chunk {}\n", i).into_bytes()); + } + }; + + Response::builder() + .body(Body::stream(stream)) + .unwrap() +} +``` + +The adapter converts EdgeZero streams to Cloudflare's `ReadableStream` format. + +## Testing + +Run contract tests for the Cloudflare adapter: + +```bash +cargo test -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown +``` + +Note: Some tests require `wasm-bindgen-test-runner` for execution. + +## Manifest Configuration + +Full `edgezero.toml` Cloudflare configuration: + +```toml +[adapters.cloudflare.adapter] +crate = "crates/my-app-adapter-cloudflare" +manifest = "crates/my-app-adapter-cloudflare/wrangler.toml" + +[adapters.cloudflare.build] +target = "wasm32-unknown-unknown" +profile = "release" + +[adapters.cloudflare.commands] +build = "cargo build --release --target wasm32-unknown-unknown -p my-app-adapter-cloudflare" +serve = "wrangler dev --config crates/my-app-adapter-cloudflare/wrangler.toml" +deploy = "wrangler publish --config crates/my-app-adapter-cloudflare/wrangler.toml" + +[adapters.cloudflare.logging] +level = "info" +``` + +## Comparison with Fastly + +| Feature | Cloudflare Workers | Fastly Compute | +|---------|-------------------|----------------| +| Target | `wasm32-unknown-unknown` | `wasm32-wasip1` | +| Outbound requests | Global `fetch` | Named backends | +| Storage | KV, Durable Objects, R2 | KV Store, Object Store | +| Logging | `console.log` | Log endpoints | +| CLI | Wrangler | Fastly CLI | + +## Next Steps + +- Learn about [Fastly Compute](/guide/adapters/fastly) as an alternative +- Explore the [Axum adapter](/guide/adapters/axum) for local development diff --git a/docs/guide/adapters/fastly.md b/docs/guide/adapters/fastly.md new file mode 100644 index 0000000..13b95d4 --- /dev/null +++ b/docs/guide/adapters/fastly.md @@ -0,0 +1,224 @@ +# Fastly Compute@Edge + +Deploy EdgeZero applications to Fastly's Compute@Edge platform using WebAssembly. + +## Prerequisites + +- [Fastly CLI](https://developer.fastly.com/learning/compute/#install-the-fastly-cli) +- Rust `wasm32-wasip1` target: `rustup target add wasm32-wasip1` +- [Wasmtime](https://wasmtime.dev/) or [Viceroy](https://github.com/fastly/Viceroy) for local testing + +## Project Setup + +When scaffolding with `edgezero new my-app --adapters fastly`, you get: + +``` +crates/my-app-adapter-fastly/ +├── Cargo.toml +├── fastly.toml +└── src/ + └── main.rs +``` + +### fastly.toml + +The Fastly manifest configures your service: + +```toml +manifest_version = 2 +name = "my-app" +language = "rust" +authors = ["you@example.com"] + +[local_server] + [local_server.backends] + [local_server.backends."origin"] + url = "https://your-origin.example.com" +``` + +### Entrypoint + +The Fastly entrypoint wires the adapter: + +```rust +use edgezero_adapter_fastly::{dispatch, init_logger}; +use my_app_core::App; + +#[fastly::main] +async fn main(req: fastly::Request) -> Result { + init_logger(); + let app = App::build(); + dispatch(&app, req).await +} +``` + +## Building + +Build for Fastly's Wasm target: + +```bash +# Using the CLI +edgezero build --adapter fastly + +# Or directly with cargo +cargo build -p my-app-adapter-fastly --target wasm32-wasip1 --release +``` + +The compiled Wasm binary is placed in `target/wasm32-wasip1/release/`. + +## Local Development + +Run locally with Viceroy (Fastly's local simulator): + +```bash +# Using the CLI +edgezero serve --adapter fastly + +# Or directly +fastly compute serve --skip-build +``` + +This starts a local server at `http://127.0.0.1:7676`. + +## Deployment + +Deploy to Fastly Compute@Edge: + +```bash +# Using the CLI +edgezero deploy --adapter fastly + +# Or directly +fastly compute deploy +``` + +## Backends + +Fastly routes outbound requests through named backends. Configure them in `fastly.toml`: + +```toml +[local_server.backends] + [local_server.backends."api"] + url = "https://api.example.com" + + [local_server.backends."cdn"] + url = "https://cdn.example.com" +``` + +Use backends in your proxy code: + +```rust +use edgezero_adapter_fastly::FastlyProxyClient; + +let client = FastlyProxyClient::new("api"); +let response = ProxyService::new(client).forward(request).await?; +``` + +## Logging + +Fastly uses endpoint-based logging. Initialize the logger in your entrypoint: + +```rust +use edgezero_adapter_fastly::init_logger; + +fn main() { + init_logger(); // Uses stdout by default + // or with custom endpoint: + // init_logger_with_endpoint("my-logging-endpoint"); +} +``` + +Configure logging in `edgezero.toml`: + +```toml +[adapters.fastly.logging] +endpoint = "stdout" +level = "info" +echo_stdout = true +``` + +## Context Access + +Access Fastly-specific APIs via the request context: + +```rust +use edgezero_adapter_fastly::FastlyRequestContext; + +#[action] +async fn handler(RequestContext(ctx): RequestContext) -> Response { + // Access Fastly context from extensions + if let Some(fastly_ctx) = ctx.extensions().get::() { + let client_ip = fastly_ctx.client_ip(); + let geo = fastly_ctx.geo(); + // ... + } + + // ... +} +``` + +## Streaming + +Fastly supports native streaming via `stream_to_client`: + +```rust +#[action] +async fn stream() -> Response { + let stream = async_stream::stream! { + for i in 0..100 { + yield Ok::<_, std::io::Error>(format!("chunk {}\n", i).into_bytes()); + } + }; + + Response::builder() + .body(Body::stream(stream)) + .unwrap() +} +``` + +The adapter automatically uses Fastly's streaming APIs for optimal performance. + +## Testing + +Run contract tests for the Fastly adapter: + +```bash +# Set up the Wasm runner +export CARGO_TARGET_WASM32_WASIP1_RUNNER="wasmtime run --dir=." + +# Run tests +cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 +``` + +::: tip Viceroy Issues +If Viceroy reports keychain access errors on macOS, use Wasmtime as the test runner instead. +::: + +## Manifest Configuration + +Full `edgezero.toml` Fastly configuration: + +```toml +[adapters.fastly.adapter] +crate = "crates/my-app-adapter-fastly" +manifest = "crates/my-app-adapter-fastly/fastly.toml" + +[adapters.fastly.build] +target = "wasm32-wasip1" +profile = "release" + +[adapters.fastly.commands] +build = "cargo build --release --target wasm32-wasip1 -p my-app-adapter-fastly" +serve = "fastly compute serve -C crates/my-app-adapter-fastly" +deploy = "fastly compute deploy -C crates/my-app-adapter-fastly" + +[adapters.fastly.logging] +endpoint = "stdout" +level = "info" +echo_stdout = true +``` + +## Next Steps + +- Learn about [Cloudflare Workers](/guide/adapters/cloudflare) as an alternative deployment target +- Explore [Configuration](/guide/configuration) for manifest details diff --git a/docs/guide/adapters/overview.md b/docs/guide/adapters/overview.md new file mode 100644 index 0000000..bd04ca3 --- /dev/null +++ b/docs/guide/adapters/overview.md @@ -0,0 +1,105 @@ +# Adapter Overview + +Adapters bridge provider-specific HTTP primitives into EdgeZero's portable model. This document defines the contract that all adapters must fulfill. + +## Goals + +Adapters translate provider-specific HTTP primitives into the portable `App` in `edgezero-core`. They must: + +- Preserve request semantics +- Stream responses without buffering +- Expose provider context +- Offer a proxy bridge so handlers can forward traffic without knowing which platform they are on + +## Request Conversion + +Each adapter exposes an `into_core_request` helper that accepts the provider's request type and returns `edgezero_core::http::Request`. The conversion must: + +- **Preserve the HTTP method** exactly (`GET`, `POST`, etc.) +- **Parse the full URI** (path and query string) into an `http::Uri`. Reject invalid URIs with `EdgeError::bad_request` +- **Copy all headers** into the core request. Provider-specific headers may be filtered only when they clash with platform defaults +- **Consume the request body** into an `edgezero_core::body::Body`. If the provider offers a streaming API, it should be exposed via `Body::Stream`; otherwise a single buffered chunk is acceptable +- **Insert a provider context struct** (e.g., `FastlyRequestContext`) into the request extensions. The context should expose metadata such as client IP addresses or environment handles so handlers can reach platform APIs + +## Response Conversion + +Adapters also expose `from_core_response` (or equivalent) to transform an `edgezero_core::http::Response` into the provider response type. Implementations must: + +- **Map HTTP status codes** verbatim +- **Copy headers**, respecting casing rules enforced by the provider +- **Preserve streaming bodies** - `Body::Stream` should be written chunk-by-chunk to the provider output without buffering the entire payload +- **Handle encoding helpers** (`decode_gzip_stream`, `decode_brotli_stream`) where a provider requires transparent decompression + +## Dispatch Helper + +Adapters surface a `dispatch` function that bridges from the provider event loop into the shared router (`App::router().oneshot(...)`). It should: + +1. Convert the incoming provider request with `into_core_request` +2. Await the router future +3. Convert the resulting `Response` back into the provider type +4. Map any `EdgeError` into the provider's error type so failures surface as HTTP 5xx responses instead of panicking + +This helper is what demo entrypoints and adapters call when wiring their platform-specific main functions. + +## Proxy Integration + +Adapters implement `edgezero_core::proxy::ProxyClient` so handlers can forward outbound requests. The client must: + +- Accept a `ProxyRequest` created with `ProxyRequest::from_request` +- Build and send an outbound provider request, reusing headers and streaming the body without buffering +- Convert the provider response into a `ProxyResponse`, again preserving streaming behaviour and normalising encodings +- Attach a diagnostic header (e.g., `x-edgezero-proxy`) identifying which adapter forwarded the call +- Surface provider errors as `EdgeError::internal` so applications can decide how to respond + +## Logging Initialisation + +Each adapter exports an `init_logger` helper for platform-specific logging backends (`log_fastly` or `console_log!`). Applications should call it before building the router. New adapters should provide a comparable helper so apps consistently opt into logging. + +## Contract Tests + +To keep the contract enforceable, each adapter includes integration tests that validate request/response conversions and the dispatch helper: + +- `into_core_request` for method, URI, header, body, and context propagation +- `from_core_response` for status propagation and streamed body writes +- `dispatch` for routed handlers, body passthrough, and streaming responses + +### Fastly Tests + +Because the Fastly SDK links against the Compute@Edge host functions, the contract tests compile only for `wasm32-wasip1`. Run them with: + +```bash +rustup target add wasm32-wasip1 +cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --tests +``` + +Provide a Wasm runner (Wasmtime or Viceroy) via `CARGO_TARGET_WASM32_WASIP1_RUNNER` if you want to execute the binaries instead of running `--no-run`. + +### Cloudflare Tests + +Cloudflare's adapter relies on `wasm32-unknown-unknown`. The contract suite uses `wasm-bindgen-test` to run under the Workers runtime shims: + +```bash +rustup target add wasm32-unknown-unknown +cargo test -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown --tests +``` + +## Onboarding New Adapters + +When bringing up another adapter: + +1. **Implement request/response conversion functions** that follow the rules above +2. **Provide a context type** exposing the adapter's metadata and insert it in `into_core_request` +3. **Implement a `dispatch` wrapper** plus logging helper +4. **Wire up a `ProxyClient`** that streams bodies and normalises encodings +5. **Copy the contract test suite**, swapping in the new adapter types. Ensure the tests are gated to the target architecture if the adapter SDK does not compile for native hosts +6. **Register the adapter** with `edgezero-adapter::register_adapter` (typically in a `cli` module using the `ctor` crate) so the CLI can discover it dynamically + +Adapters that fulfil these steps can be dropped into the EdgeZero CLI without requiring changes to application code. + +## Available Adapters + +| Adapter | Platform | Target | Status | +|---------|----------|--------|--------| +| [Fastly](/guide/adapters/fastly) | Fastly Compute@Edge | `wasm32-wasip1` | Stable | +| [Cloudflare](/guide/adapters/cloudflare) | Cloudflare Workers | `wasm32-unknown-unknown` | Stable | +| [Axum](/guide/adapters/axum) | Native (Tokio) | Host | Stable | diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md new file mode 100644 index 0000000..940c6b3 --- /dev/null +++ b/docs/guide/architecture.md @@ -0,0 +1,134 @@ +# Architecture + +EdgeZero is organized as a Cargo workspace with distinct crates for core functionality, platform adapters, and tooling. + +## Workspace Layout + +``` +edgezero/ +├── crates/ +│ ├── edgezero-core/ # Core routing, extractors, middleware +│ ├── edgezero-macros/ # Procedural macros (#[action], app!) +│ ├── edgezero-adapter/ # Shared adapter traits and registry +│ ├── edgezero-adapter-fastly/ # Fastly Compute@Edge bridge +│ ├── edgezero-adapter-cloudflare/ # Cloudflare Workers bridge +│ ├── edgezero-adapter-axum/ # Native Axum/Tokio bridge +│ └── edgezero-cli/ # CLI for scaffolding and dev server +└── examples/ + └── app-demo/ # Reference application +``` + +## Core Crate + +`edgezero-core` provides the runtime-agnostic foundation: + +- **Routing** - `RouterService` with path parameter matching via `matchit` +- **Request/Response** - Portable `http::Request` and `http::Response` types +- **Body** - Unified body type supporting buffered and streaming modes +- **Extractors** - `Json`, `Path`, `Query`, `ValidatedQuery` +- **Middleware** - Composable middleware chain with async support +- **Manifest** - `edgezero.toml` parsing and validation +- **Compression** - Shared gzip/brotli stream decoders + +Handlers in your core crate only depend on `edgezero-core`, keeping them portable. + +## Macros Crate + +`edgezero-macros` provides compile-time code generation: + +- **`#[action]`** - Transforms async functions into handlers with automatic extractor wiring +- **`app!`** - Generates router setup from your `edgezero.toml` manifest + +Example usage: + +```rust +// In your core crate's lib.rs +mod handlers; + +edgezero_core::app!("../../edgezero.toml"); +``` + +## Adapter Crates + +Adapters translate between provider-specific types and the portable core model: + +### edgezero-adapter-fastly + +- Converts Fastly `Request` to `edgezero_core::http::Request` +- Maps core responses back to Fastly `Response` +- Provides `FastlyRequestContext` for accessing Fastly-specific APIs +- Implements `FastlyProxyClient` for upstream requests + +### edgezero-adapter-cloudflare + +- Converts Workers `Request` to core request +- Maps responses to Workers `Response` +- Provides `CloudflareRequestContext` for Workers APIs +- Implements `CloudflareProxyClient` for fetch operations + +### edgezero-adapter-axum + +- Wraps `RouterService` in Axum/Tokio services +- Powers the local development server +- Supports native container deployments + +## CLI Crate + +`edgezero-cli` provides the `edgezero` binary: + +- **`edgezero new`** - Scaffolds a new project with templates +- **`edgezero dev`** - Runs the local Axum dev server +- **`edgezero build`** - Builds for a specific adapter target +- **`edgezero serve`** - Runs provider-specific local servers (Viceroy, wrangler dev) +- **`edgezero deploy`** - Deploys to production + +## Data Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Provider Runtime │ +│ (Fastly Compute / Cloudflare Workers / Axum Server) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Adapter │ +│ - into_core_request(): Provider Request → Core Request │ +│ - from_core_response(): Core Response → Provider Response │ +│ - dispatch(): Full request lifecycle │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ edgezero-core │ +│ - RouterService matches routes │ +│ - Middleware chain executes │ +│ - Handler runs with extracted params │ +│ - Response built and returned │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Your Core Crate │ +│ - handlers.rs: Business logic │ +│ - lib.rs: App definition via app! macro │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Feature Flags + +EdgeZero uses feature flags for optional functionality: + +| Feature | Crate | Purpose | +|---------|-------|---------| +| `json` | edgezero-core | JSON extractor/responder support | +| `form` | edgezero-core | Form data extraction | +| `validator` | edgezero-core | Validated extractors | +| `fastly` | edgezero-adapter-fastly | Fastly SDK integration | +| `cloudflare` | edgezero-adapter-cloudflare | Workers SDK integration | +| `dev-example` | edgezero-cli | Bundled demo app for development | + +## Next Steps + +- Learn about the [Adapter Contract](/guide/adapters/overview) for extending EdgeZero +- Explore [Configuration](/guide/configuration) to customize your app diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md new file mode 100644 index 0000000..dfbcca0 --- /dev/null +++ b/docs/guide/cli-reference.md @@ -0,0 +1,247 @@ +# CLI Reference + +The `edgezero` CLI provides commands for scaffolding, development, building, and deployment. + +## Installation + +Install from the workspace: + +```bash +cargo install --path crates/edgezero-cli +``` + +Or from a published crate: + +```bash +cargo install edgezero-cli +``` + +## Commands + +### edgezero new + +Scaffold a new EdgeZero project: + +```bash +edgezero new [options] +``` + +**Arguments:** +- `` - Project name (used for directory and crate names) + +**Options:** +- `--adapters ` - Comma-separated adapters to include (default: `fastly,cloudflare,axum`) + +**Examples:** + +```bash +# Create project with all adapters +edgezero new my-app + +# Create project with specific adapters +edgezero new my-app --adapters fastly,axum + +# Create project with only Cloudflare +edgezero new my-app --adapters cloudflare +``` + +**Generated structure:** + +``` +my-app/ +├── Cargo.toml +├── edgezero.toml +├── crates/ +│ ├── my-app-core/ +│ ├── my-app-adapter-fastly/ (if --adapters includes fastly) +│ ├── my-app-adapter-cloudflare/ (if --adapters includes cloudflare) +│ └── my-app-adapter-axum/ (if --adapters includes axum) +``` + +### edgezero dev + +Start the local development server: + +```bash +edgezero dev [options] +``` + +**Options:** +- `--port ` - Port to listen on (default: 8787) +- `--host ` - Host to bind to (default: 127.0.0.1) + +**Examples:** + +```bash +# Start dev server with defaults +edgezero dev + +# Start on custom port +edgezero dev --port 3000 + +# Bind to all interfaces +edgezero dev --host 0.0.0.0 +``` + +The dev server uses the Axum adapter and reads configuration from `edgezero.toml`. + +### edgezero build + +Build for a specific adapter: + +```bash +edgezero build --adapter +``` + +**Arguments:** +- `--adapter ` - Target adapter (`fastly`, `cloudflare`, `axum`) + +**Examples:** + +```bash +# Build for Fastly +edgezero build --adapter fastly + +# Build for Cloudflare +edgezero build --adapter cloudflare + +# Build native binary +edgezero build --adapter axum +``` + +The command executes the `build` command from `[adapters..commands]` in `edgezero.toml`, or falls back to the built-in adapter helper. + +### edgezero serve + +Run the provider-specific local server: + +```bash +edgezero serve --adapter +``` + +**Arguments:** +- `--adapter ` - Target adapter (`fastly`, `cloudflare`, `axum`) + +**Examples:** + +```bash +# Run Fastly's Viceroy +edgezero serve --adapter fastly + +# Run Wrangler dev server +edgezero serve --adapter cloudflare + +# Run native Axum server +edgezero serve --adapter axum +``` + +**Provider behavior:** +- **Fastly**: Runs `fastly compute serve` +- **Cloudflare**: Runs `wrangler dev` +- **Axum**: Runs `cargo run -p ` + +### edgezero deploy + +Deploy to production: + +```bash +edgezero deploy --adapter +``` + +**Arguments:** +- `--adapter ` - Target adapter (`fastly`, `cloudflare`) + +**Examples:** + +```bash +# Deploy to Fastly +edgezero deploy --adapter fastly + +# Deploy to Cloudflare +edgezero deploy --adapter cloudflare +``` + +**Provider behavior:** +- **Fastly**: Runs `fastly compute deploy` +- **Cloudflare**: Runs `wrangler publish` + +::: warning +The `axum` adapter doesn't support `deploy` - use standard container/binary deployment instead. +::: + +## Environment Variables + +The CLI respects these environment variables: + +| Variable | Description | +|----------|-------------| +| `RUST_LOG` | Log level for dev server | +| `EDGEZERO_MANIFEST` | Path to manifest (default: `edgezero.toml`) | + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | General error | +| 2 | Missing configuration | +| 3 | Build failure | +| 4 | Missing adapter | + +## Working Directory + +All commands expect to run from the project root where `edgezero.toml` is located. The CLI searches up the directory tree for the manifest if not found in the current directory. + +## Adapter Discovery + +Adapters register themselves via the `edgezero-adapter` registry. The CLI discovers available adapters at runtime: + +```bash +# List available adapters +edgezero --list-adapters +``` + +Built-in adapters: +- `fastly` - Fastly Compute@Edge +- `cloudflare` - Cloudflare Workers +- `axum` - Native Axum/Tokio + +## Troubleshooting + +### Missing Wasm Target + +``` +error: target may not be installed +``` + +Install the required target: +```bash +rustup target add wasm32-wasip1 # For Fastly +rustup target add wasm32-unknown-unknown # For Cloudflare +``` + +### Manifest Not Found + +``` +error: edgezero.toml not found +``` + +Ensure you're in the project root or set `EDGEZERO_MANIFEST`: +```bash +EDGEZERO_MANIFEST=/path/to/edgezero.toml edgezero dev +``` + +### Provider CLI Not Found + +``` +error: fastly: command not found +``` + +Install the provider CLI: +- Fastly: https://developer.fastly.com/learning/compute/ +- Cloudflare: `npm install -g wrangler` + +## Next Steps + +- Configure your project with [edgezero.toml](/guide/configuration) +- Deploy to [Fastly](/guide/adapters/fastly) or [Cloudflare](/guide/adapters/cloudflare) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 0000000..8fb099f --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1,319 @@ +# Configuration + +The `edgezero.toml` manifest describes an EdgeZero application, providing a single source of truth for routing, middleware, adapters, and environment configuration. + +## Overview + +New workspaces scaffolded with `edgezero new` include this manifest by default. The manifest drives both runtime routing and CLI commands. + +```toml +[app] +name = "my-app" +version = "0.1.0" +kind = "http" +entry = "crates/my-app-core" +middleware = ["edgezero_core::middleware::RequestLogger"] + +[[triggers.http]] +id = "root" +path = "/" +methods = ["GET"] +handler = "my_app_core::handlers::root" + +[adapters.fastly] +# Fastly-specific configuration + +[adapters.cloudflare] +# Cloudflare-specific configuration +``` + +## App Section + +The `[app]` section defines application metadata: + +```toml +[app] +name = "demo" +version = "0.1.0" +kind = "http" +entry = "crates/demo-core" +middleware = ["edgezero_core::middleware::RequestLogger"] +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Display name for the application | +| `version` | No | Semantic version | +| `kind` | No | Application type (currently only `http`) | +| `entry` | Yes | Path to the core crate containing handlers | +| `middleware` | No | List of middleware to apply globally | + +### Middleware + +Manifest-driven middleware are applied in order before routes: + +```toml +[app] +middleware = [ + "edgezero_core::middleware::RequestLogger", + "my_app_core::cors::Cors" +] +``` + +Each item must be: +- A publicly accessible path +- Either a unit struct or zero-argument constructor +- Implementing `edgezero_core::middleware::Middleware` + +## HTTP Triggers + +The `[[triggers.http]]` array defines routes: + +```toml +[[triggers.http]] +id = "root" +path = "/" +methods = ["GET"] +handler = "my_app_core::handlers::root" + +[[triggers.http]] +id = "echo" +path = "/echo/{name}" +methods = ["GET", "POST"] +handler = "my_app_core::handlers::echo" +adapters = ["fastly", "cloudflare"] +body-mode = "buffered" +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `id` | No | Stable identifier for tooling | +| `path` | Yes | URI template (`{param}` for params, `{*rest}` for catch-all) | +| `methods` | No | Allowed HTTP methods (defaults to `GET`) | +| `handler` | Yes | Path to handler function | +| `adapters` | No | Which adapters expose this route (empty = all) | +| `body-mode` | No | `buffered` or `stream` | + +## Environment Section + +Declare environment variables and secrets: + +```toml +[environment] + +[[environment.variables]] +name = "API_BASE_URL" +env = "API_BASE_URL" +value = "https://example.com/api" + +[[environment.secrets]] +name = "API_TOKEN" +adapters = ["fastly", "cloudflare"] +env = "API_TOKEN" +``` + +### Variables + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Variable name in application | +| `env` | No | Environment key (defaults to `name`) | +| `value` | No | Default value | +| `adapters` | No | Limit to specific adapters | + +Variables with a default `value` are injected when running CLI commands. + +### Secrets + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Secret name in application | +| `env` | No | Environment key (defaults to `name`) | +| `adapters` | No | Limit to specific adapters | + +Secrets must be present in the environment; missing secrets abort CLI commands with an error. + +## Adapters Section + +Each adapter has its own configuration block: + +```toml +[adapters.fastly.adapter] +crate = "crates/demo-adapter-fastly" +manifest = "crates/demo-adapter-fastly/fastly.toml" + +[adapters.fastly.build] +target = "wasm32-wasip1" +profile = "release" + +[adapters.fastly.commands] +build = "cargo build --release --target wasm32-wasip1 -p demo-adapter-fastly" +serve = "fastly compute serve -C crates/demo-adapter-fastly" +deploy = "fastly compute deploy -C crates/demo-adapter-fastly" + +[adapters.fastly.logging] +endpoint = "stdout" +level = "info" +echo_stdout = true +``` + +### Adapter Metadata + +| Field | Description | +|-------|-------------| +| `crate` | Path to adapter crate | +| `manifest` | Path to provider manifest (fastly.toml, wrangler.toml) | + +### Build Configuration + +| Field | Description | +|-------|-------------| +| `target` | Rust compilation target | +| `profile` | Build profile (`release`, `dev`) | +| `features` | Cargo features to enable | + +### Commands + +| Field | Description | +|-------|-------------| +| `build` | Command for `edgezero build --adapter ` | +| `serve` | Command for `edgezero serve --adapter ` | +| `deploy` | Command for `edgezero deploy --adapter ` | + +When commands are omitted, the CLI falls back to built-in adapter helpers. + +### Logging + +| Field | Adapters | Description | +|-------|----------|-------------| +| `endpoint` | Fastly | Log endpoint name | +| `level` | All | Log level: `trace`, `debug`, `info`, `warn`, `error`, `off` | +| `echo_stdout` | Fastly | Mirror logs to stdout | + +## Full Example + +```toml +[app] +name = "my-app" +version = "0.1.0" +kind = "http" +entry = "crates/my-app-core" +middleware = [ + "edgezero_core::middleware::RequestLogger", + "my_app_core::middleware::Cors" +] + +[[triggers.http]] +id = "root" +path = "/" +methods = ["GET"] +handler = "my_app_core::handlers::root" + +[[triggers.http]] +id = "echo" +path = "/echo/{name}" +methods = ["GET"] +handler = "my_app_core::handlers::echo" + +[[triggers.http]] +id = "api" +path = "/api/{*rest}" +methods = ["GET", "POST", "PUT", "DELETE"] +handler = "my_app_core::handlers::api_proxy" +body-mode = "stream" + +[environment] + +[[environment.variables]] +name = "API_URL" +value = "https://api.example.com" + +[[environment.secrets]] +name = "API_KEY" + +[adapters.fastly.adapter] +crate = "crates/my-app-adapter-fastly" +manifest = "crates/my-app-adapter-fastly/fastly.toml" + +[adapters.fastly.build] +target = "wasm32-wasip1" +profile = "release" + +[adapters.fastly.commands] +build = "cargo build --release --target wasm32-wasip1 -p my-app-adapter-fastly" +serve = "fastly compute serve -C crates/my-app-adapter-fastly" +deploy = "fastly compute deploy -C crates/my-app-adapter-fastly" + +[adapters.fastly.logging] +endpoint = "stdout" +level = "info" +echo_stdout = true + +[adapters.cloudflare.adapter] +crate = "crates/my-app-adapter-cloudflare" +manifest = "crates/my-app-adapter-cloudflare/wrangler.toml" + +[adapters.cloudflare.build] +target = "wasm32-unknown-unknown" +profile = "release" + +[adapters.cloudflare.commands] +build = "cargo build --release --target wasm32-unknown-unknown -p my-app-adapter-cloudflare" +serve = "wrangler dev --config crates/my-app-adapter-cloudflare/wrangler.toml" +deploy = "wrangler publish --config crates/my-app-adapter-cloudflare/wrangler.toml" + +[adapters.cloudflare.logging] +level = "info" + +[adapters.axum.adapter] +crate = "crates/my-app-adapter-axum" +manifest = "crates/my-app-adapter-axum/axum.toml" + +[adapters.axum.commands] +build = "cargo build --release -p my-app-adapter-axum" +serve = "cargo run -p my-app-adapter-axum" +``` + +## Using the Manifest + +### app! Macro + +Generate router wiring from the manifest: + +```rust +// In your core crate's lib.rs +mod handlers; + +edgezero_core::app!("../../edgezero.toml"); +``` + +The macro: +- Parses HTTP triggers +- Generates route registration +- Wires middleware from the manifest +- Creates the `App` struct with `build()` method + +### ManifestLoader + +Load the manifest programmatically: + +```rust +use edgezero_core::manifest::ManifestLoader; + +let manifest = ManifestLoader::load("edgezero.toml")?; +println!("App name: {}", manifest.app.name); +``` + +## Validation + +`ManifestLoader` validates: +- Non-empty trigger paths and handlers +- Well-formed logging levels +- Required fields present + +Errors are surfaced at startup or during macro expansion. + +## Next Steps + +- Learn about [CLI commands](/guide/cli-reference) +- Explore [adapter-specific configuration](/guide/adapters/overview) diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 0000000..6b13468 --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,119 @@ +# Getting Started + +This guide walks you through creating your first EdgeZero application. + +## Prerequisites + +- Rust toolchain (1.70+) +- For Fastly: `wasm32-wasip1` target and the Fastly CLI +- For Cloudflare: `wasm32-unknown-unknown` target and Wrangler + +## Installation + +Install the EdgeZero CLI from the workspace or a published crate: + +```bash +cargo install --path crates/edgezero-cli +``` + +## Create a New Project + +Scaffold a new EdgeZero app targeting your preferred adapters: + +```bash +# Create an app with Fastly, Cloudflare, and Axum adapters +edgezero new my-app --adapters fastly cloudflare axum +cd my-app +``` + +This generates a workspace with: +- `crates/my-app-core` - Your shared handlers and routing logic +- `crates/my-app-adapter-fastly` - Fastly Compute entrypoint +- `crates/my-app-adapter-cloudflare` - Cloudflare Workers entrypoint +- `crates/my-app-adapter-axum` - Native Axum entrypoint +- `edgezero.toml` - Manifest describing routes, middleware, and adapter config + +## Start the Dev Server + +Run the local Axum-powered development server: + +```bash +edgezero dev +``` + +Your app is now running at `http://127.0.0.1:8787`. Try the generated endpoints: + +```bash +# Root endpoint +curl http://127.0.0.1:8787/ + +# Path parameter extraction +curl http://127.0.0.1:8787/echo/alice + +# JSON echo +curl -X POST http://127.0.0.1:8787/echo \ + -H "Content-Type: application/json" \ + -d '{"name": "Bob"}' +``` + +## Project Structure + +A scaffolded project looks like this: + +``` +my-app/ +├── Cargo.toml # Workspace manifest +├── edgezero.toml # EdgeZero configuration +├── crates/ +│ ├── my-app-core/ +│ │ ├── Cargo.toml +│ │ └── src/ +│ │ ├── lib.rs # App definition with edgezero_core::app! +│ │ └── handlers.rs # Your route handlers +│ ├── my-app-adapter-fastly/ +│ │ ├── Cargo.toml +│ │ ├── fastly.toml +│ │ └── src/main.rs +│ ├── my-app-adapter-cloudflare/ +│ │ ├── Cargo.toml +│ │ ├── wrangler.toml +│ │ └── src/main.rs +│ └── my-app-adapter-axum/ +│ ├── Cargo.toml +│ ├── axum.toml +│ └── src/main.rs +``` + +## Writing Your First Handler + +Handlers use the `#[action]` macro for ergonomic extractor support: + +```rust +use edgezero_core::action; +use edgezero_core::extractor::Json; +use edgezero_core::response::Text; + +#[derive(serde::Deserialize)] +struct EchoBody { + name: String, +} + +#[action] +async fn echo_json(Json(body): Json) -> Text { + Text::new(format!("Hello, {}!", body.name)) +} +``` + +## Running Tests + +Run your workspace tests with: + +```bash +cargo test +``` + +## Next Steps + +- Learn about [Routing](/guide/routing) to define your endpoints +- Explore [Handlers & Extractors](/guide/handlers) for type-safe request handling +- Deploy to [Fastly](/guide/adapters/fastly) or [Cloudflare](/guide/adapters/cloudflare) diff --git a/docs/guide/handlers.md b/docs/guide/handlers.md new file mode 100644 index 0000000..6e6442f --- /dev/null +++ b/docs/guide/handlers.md @@ -0,0 +1,243 @@ +# Handlers & Extractors + +EdgeZero provides ergonomic handler definitions using the `#[action]` macro and type-safe extractors. + +## The #[action] Macro + +The `#[action]` macro transforms async functions into EdgeZero handlers with automatic extractor wiring: + +```rust +use edgezero_core::action; +use edgezero_core::extractor::Json; +use edgezero_core::response::Text; + +#[derive(serde::Deserialize)] +struct CreateUser { + name: String, + email: String, +} + +#[action] +async fn create_user(Json(body): Json) -> Text { + Text::new(format!("Created user: {}", body.name)) +} +``` + +The macro: +- Generates the `FromRequest` boilerplate for each extractor +- Handles async execution +- Converts the return type into a proper response + +## Built-in Extractors + +### Path Parameters + +Extract typed parameters from the URL path: + +```rust +use edgezero_core::extractor::Path; + +// Single parameter +#[action] +async fn get_user(Path(id): Path) -> Text { + Text::new(format!("User ID: {}", id)) +} + +// Multiple parameters via struct +#[derive(serde::Deserialize)] +struct PostPath { + user_id: u64, + post_id: u64, +} + +#[action] +async fn get_post(Path(params): Path) -> Text { + Text::new(format!("User {} Post {}", params.user_id, params.post_id)) +} +``` + +### Query Parameters + +Extract query string parameters: + +```rust +use edgezero_core::extractor::Query; + +#[derive(serde::Deserialize)] +struct Pagination { + page: Option, + limit: Option, +} + +#[action] +async fn list_items(Query(params): Query) -> Text { + let page = params.page.unwrap_or(1); + let limit = params.limit.unwrap_or(10); + Text::new(format!("Page {} with {} items", page, limit)) +} +``` + +### JSON Body + +Parse JSON request bodies: + +```rust +use edgezero_core::extractor::Json; + +#[derive(serde::Deserialize)] +struct LoginRequest { + username: String, + password: String, +} + +#[action] +async fn login(Json(body): Json) -> Text { + Text::new(format!("Logging in: {}", body.username)) +} +``` + +### Validated Extractors + +Use `validator` crate integration for input validation: + +```rust +use edgezero_core::extractor::{ValidatedJson, ValidatedQuery}; +use validator::Validate; + +#[derive(serde::Deserialize, Validate)] +struct CreatePost { + #[validate(length(min = 1, max = 200))] + title: String, + #[validate(length(min = 1))] + content: String, +} + +#[action] +async fn create_post(ValidatedJson(body): ValidatedJson) -> Text { + Text::new(format!("Created post: {}", body.title)) +} +``` + +If validation fails, EdgeZero automatically returns a 400 Bad Request with error details. + +### Request Context + +Access the full request context for headers, method, URI, etc: + +```rust +use edgezero_core::context::RequestContext; + +#[action] +async fn inspect(RequestContext(ctx): RequestContext) -> Text { + let method = ctx.method(); + let path = ctx.uri().path(); + let user_agent = ctx.headers() + .get("user-agent") + .and_then(|v| v.to_str().ok()) + .unwrap_or("unknown"); + + Text::new(format!("{} {} from {}", method, path, user_agent)) +} +``` + +## Response Types + +### Text Responses + +```rust +use edgezero_core::response::Text; + +#[action] +async fn hello() -> Text<&'static str> { + Text::new("Hello, World!") +} +``` + +### JSON Responses + +```rust +use edgezero_core::response::Json; + +#[derive(serde::Serialize)] +struct User { + id: u64, + name: String, +} + +#[action] +async fn get_user() -> Json { + Json(User { id: 1, name: "Alice".into() }) +} +``` + +### Status Codes + +```rust +use edgezero_core::http::StatusCode; +use edgezero_core::response::Text; + +#[action] +async fn not_found() -> (StatusCode, Text<&'static str>) { + (StatusCode::NOT_FOUND, Text::new("Resource not found")) +} +``` + +### Custom Headers + +```rust +use edgezero_core::http::{HeaderMap, HeaderValue}; +use edgezero_core::response::Text; + +#[action] +async fn with_headers() -> (HeaderMap, Text<&'static str>) { + let mut headers = HeaderMap::new(); + headers.insert("x-custom", HeaderValue::from_static("value")); + (headers, Text::new("Response with custom header")) +} +``` + +## Combining Extractors + +You can use multiple extractors in a single handler: + +```rust +#[action] +async fn update_user( + Path(id): Path, + Query(params): Query, + Json(body): Json, +) -> Json { + // All three extractors are available + Json(User { id, name: body.name }) +} +``` + +## Error Handling + +Extractors return `EdgeError` on failure, which automatically converts to appropriate HTTP responses: + +| Error | Status Code | +|-------|-------------| +| JSON parse error | 400 Bad Request | +| Validation error | 400 Bad Request | +| Missing path param | 500 Internal Server Error | +| Type conversion error | 400 Bad Request | + +For custom error handling, return `Result`: + +```rust +use edgezero_core::error::EdgeError; + +#[action] +async fn fallible(Json(body): Json) -> Result, EdgeError> { + if body.invalid { + return Err(EdgeError::bad_request("Invalid request")); + } + Ok(Json(Response { success: true })) +} +``` + +## Next Steps + +- Learn about [Middleware](/guide/middleware) for request/response processing +- Explore [Streaming](/guide/streaming) for large response bodies diff --git a/docs/guide/middleware.md b/docs/guide/middleware.md new file mode 100644 index 0000000..1c85f78 --- /dev/null +++ b/docs/guide/middleware.md @@ -0,0 +1,210 @@ +# Middleware + +EdgeZero supports composable middleware for cross-cutting concerns like logging, authentication, and CORS. + +## Defining Middleware + +Middleware implements the `Middleware` trait: + +```rust +use edgezero_core::middleware::Middleware; +use edgezero_core::http::{Request, Response}; +use edgezero_core::body::Body; + +pub struct RequestLogger; + +impl Middleware for RequestLogger { + async fn handle( + &self, + req: Request, + next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, + ) -> Response { + let method = req.method().clone(); + let path = req.uri().path().to_string(); + + log::info!("--> {} {}", method, path); + + let response = next(req).await; + + log::info!("<-- {} {} {}", method, path, response.status()); + + response + } +} +``` + +## Registering Middleware + +### Via Manifest + +Define middleware in `edgezero.toml`: + +```toml +[app] +name = "my-app" +entry = "crates/my-app-core" +middleware = [ + "edgezero_core::middleware::RequestLogger", + "my_app_core::middleware::Auth" +] +``` + +Middleware are applied in order before routes are matched. + +### Programmatically + +Register middleware when building the router: + +```rust +use edgezero_core::router::RouterService; + +let router = RouterService::builder() + .middleware(RequestLogger) + .middleware(CorsMiddleware::default()) + .route(Method::GET, "/hello", hello) + .build(); +``` + +## Middleware Order + +Middleware execute in registration order for requests, and reverse order for responses: + +``` +Request Flow: + Client → Logger → Auth → CORS → Handler + +Response Flow: + Handler → CORS → Auth → Logger → Client +``` + +## Common Patterns + +### Authentication + +```rust +pub struct AuthMiddleware { + secret: String, +} + +impl Middleware for AuthMiddleware { + async fn handle( + &self, + req: Request, + next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, + ) -> Response { + // Check authorization header + let auth_header = req.headers().get("authorization"); + + match auth_header { + Some(value) if self.verify_token(value) => { + // Token valid, continue to handler + next(req).await + } + _ => { + // Return 401 Unauthorized + Response::builder() + .status(401) + .body(Body::from("Unauthorized")) + .unwrap() + } + } + } +} +``` + +### CORS + +```rust +pub struct CorsMiddleware { + allowed_origins: Vec, +} + +impl Middleware for CorsMiddleware { + async fn handle( + &self, + req: Request, + next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, + ) -> Response { + let origin = req.headers() + .get("origin") + .and_then(|v| v.to_str().ok()) + .map(String::from); + + let mut response = next(req).await; + + if let Some(origin) = origin { + if self.allowed_origins.contains(&origin) { + response.headers_mut().insert( + "access-control-allow-origin", + origin.parse().unwrap(), + ); + } + } + + response + } +} +``` + +### Request Timing + +```rust +pub struct TimingMiddleware; + +impl Middleware for TimingMiddleware { + async fn handle( + &self, + req: Request, + next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, + ) -> Response { + let start = std::time::Instant::now(); + + let mut response = next(req).await; + + let duration = start.elapsed(); + response.headers_mut().insert( + "x-response-time", + format!("{}ms", duration.as_millis()).parse().unwrap(), + ); + + response + } +} +``` + +## Early Returns + +Middleware can short-circuit the chain by not calling `next`: + +```rust +impl Middleware for RateLimiter { + async fn handle( + &self, + req: Request, + next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, + ) -> Response { + if self.is_rate_limited(&req) { + // Don't call next - return immediately + return Response::builder() + .status(429) + .body(Body::from("Too Many Requests")) + .unwrap(); + } + + next(req).await + } +} +``` + +## Built-in Middleware + +EdgeZero provides these middleware out of the box: + +| Middleware | Purpose | +|------------|---------| +| `RequestLogger` | Logs request method, path, and response status | + +## Next Steps + +- Learn about [Streaming](/guide/streaming) for progressive responses +- Explore [Proxying](/guide/proxying) for upstream forwarding diff --git a/docs/guide/proxying.md b/docs/guide/proxying.md new file mode 100644 index 0000000..2b3afb0 --- /dev/null +++ b/docs/guide/proxying.md @@ -0,0 +1,244 @@ +# Proxying + +EdgeZero provides built-in helpers for forwarding requests to upstream services while staying provider-agnostic. + +## Proxy Primitives + +The core proxy types live in `edgezero_core::proxy`: + +- **`ProxyRequest`** - Represents a request to forward upstream +- **`ProxyResponse`** - The response from the upstream service +- **`ProxyService`** - Executes proxy requests using a provider-specific client + +## Basic Proxying + +```rust +use edgezero_core::action; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{Response, Uri}; +use edgezero_core::proxy::{ProxyRequest, ProxyService}; +use edgezero_core::body::Body; + +#[action] +async fn proxy_to_api(RequestContext(ctx): RequestContext) -> Response { + let target: Uri = "https://api.example.com/v1".parse().unwrap(); + + // Build proxy request from incoming request + let proxy_request = ProxyRequest::from_request(ctx.into_request(), target); + + // Forward using the adapter's proxy client + let client = get_proxy_client(); // Platform-specific + let response = ProxyService::new(client) + .forward(proxy_request) + .await + .unwrap(); + + response.into_response() +} +``` + +## Adapter-Specific Clients + +Each adapter provides its own proxy client implementation: + +### Fastly + +```rust +use edgezero_adapter_fastly::FastlyProxyClient; + +let client = FastlyProxyClient::new("backend-name"); +let response = ProxyService::new(client).forward(request).await?; +``` + +The backend name must be configured in `fastly.toml`: + +```toml +[local_server.backends.backend-name] +url = "https://api.example.com" +``` + +### Cloudflare + +```rust +use edgezero_adapter_cloudflare::CloudflareProxyClient; + +let client = CloudflareProxyClient::new(); +let response = ProxyService::new(client).forward(request).await?; +``` + +Cloudflare Workers use the global `fetch` API. + +### Axum (Development) + +```rust +use edgezero_adapter_axum::AxumProxyClient; + +let client = AxumProxyClient::new(); +let response = ProxyService::new(client).forward(request).await?; +``` + +## Request Modification + +Modify requests before forwarding: + +```rust +#[action] +async fn proxy_with_auth(RequestContext(ctx): RequestContext) -> Response { + let target: Uri = "https://api.example.com".parse().unwrap(); + + let mut proxy_request = ProxyRequest::from_request(ctx.into_request(), target); + + // Add authentication header + proxy_request.headers_mut().insert( + "authorization", + "Bearer secret-token".parse().unwrap(), + ); + + // Remove sensitive headers + proxy_request.headers_mut().remove("cookie"); + + let client = get_proxy_client(); + ProxyService::new(client).forward(proxy_request).await?.into_response() +} +``` + +## Response Processing + +Process upstream responses before returning: + +```rust +#[action] +async fn proxy_with_transform(RequestContext(ctx): RequestContext) -> Response { + let target: Uri = "https://api.example.com".parse().unwrap(); + let proxy_request = ProxyRequest::from_request(ctx.into_request(), target); + + let client = get_proxy_client(); + let mut response = ProxyService::new(client).forward(proxy_request).await?; + + // Add cache headers + response.headers_mut().insert( + "cache-control", + "public, max-age=3600".parse().unwrap(), + ); + + // Add diagnostic header + response.headers_mut().insert( + "x-proxy-by", + "edgezero".parse().unwrap(), + ); + + response.into_response() +} +``` + +## Streaming Proxies + +Proxy requests preserve streaming bodies without buffering: + +```rust +// Large uploads/downloads stream through without loading into memory +let proxy_request = ProxyRequest::from_request(request, target); +let response = proxy.forward(proxy_request).await?; +// response.body is still streaming +``` + +## Transparent Decompression + +Proxied responses are automatically decompressed: + +| Content-Encoding | Handling | +|------------------|----------| +| `gzip` | Automatically decoded | +| `br` (brotli) | Automatically decoded | +| `identity` | Passed through | + +This allows you to process response bodies without manual decompression: + +```rust +let response = proxy.forward(request).await?; +let body = response.body().bytes().await?; +// body is already decompressed, ready for transformation +``` + +## Error Handling + +Proxy operations can fail for various reasons: + +```rust +use edgezero_core::error::EdgeError; + +#[action] +async fn safe_proxy(RequestContext(ctx): RequestContext) -> Response { + let target: Uri = "https://api.example.com".parse().unwrap(); + let proxy_request = ProxyRequest::from_request(ctx.into_request(), target); + + let client = get_proxy_client(); + match ProxyService::new(client).forward(proxy_request).await { + Ok(response) => response.into_response(), + Err(e) => { + log::error!("Proxy failed: {}", e); + Response::builder() + .status(502) + .body(Body::from("Bad Gateway")) + .unwrap() + } + } +} +``` + +## Common Use Cases + +### API Gateway + +```rust +#[action] +async fn api_gateway( + Path(service): Path, + RequestContext(ctx): RequestContext, +) -> Response { + let target = match service.as_str() { + "users" => "https://users-api.internal", + "orders" => "https://orders-api.internal", + _ => return Response::builder() + .status(404) + .body(Body::from("Service not found")) + .unwrap(), + }; + + let uri: Uri = target.parse().unwrap(); + let request = ProxyRequest::from_request(ctx.into_request(), uri); + + get_proxy_client() + .forward(request) + .await + .map(|r| r.into_response()) + .unwrap_or_else(|_| bad_gateway()) +} +``` + +### Caching Proxy + +```rust +#[action] +async fn caching_proxy(RequestContext(ctx): RequestContext) -> Response { + // Check cache first + if let Some(cached) = cache.get(ctx.uri().path()) { + return cached; + } + + // Proxy to origin + let response = proxy.forward(request).await?; + + // Cache successful responses + if response.status().is_success() { + cache.set(ctx.uri().path(), response.clone()); + } + + response.into_response() +} +``` + +## Next Steps + +- Configure backends in [Fastly](/guide/adapters/fastly) adapter guide +- Learn about [Cloudflare](/guide/adapters/cloudflare) fetch integration diff --git a/docs/guide/routing.md b/docs/guide/routing.md new file mode 100644 index 0000000..81470e1 --- /dev/null +++ b/docs/guide/routing.md @@ -0,0 +1,163 @@ +# Routing + +EdgeZero uses `matchit` 0.8+ for high-performance path matching with support for parameters and catch-all segments. + +## Defining Routes + +Routes are typically defined in your `edgezero.toml` manifest and wired automatically via the `app!` macro: + +```toml +[[triggers.http]] +id = "hello" +path = "/hello" +methods = ["GET"] +handler = "my_app_core::handlers::hello" + +[[triggers.http]] +id = "echo" +path = "/echo/{name}" +methods = ["GET", "POST"] +handler = "my_app_core::handlers::echo" +``` + +You can also build routes programmatically: + +```rust +use edgezero_core::router::RouterService; +use edgezero_core::http::Method; + +let router = RouterService::builder() + .route(Method::GET, "/hello", hello_handler) + .route(Method::GET, "/echo/{name}", echo_handler) + .route(Method::POST, "/echo", echo_json_handler) + .build(); +``` + +## Path Parameters + +Define parameters with `{name}` segments: + +```rust +use edgezero_core::action; +use edgezero_core::extractor::Path; +use edgezero_core::response::Text; + +#[action] +async fn greet(Path(name): Path) -> Text { + Text::new(format!("Hello, {}!", name)) +} +``` + +For routes like `/users/{id}/posts/{post_id}`, extract multiple parameters: + +```rust +#[derive(serde::Deserialize)] +struct PostParams { + id: u64, + post_id: u64, +} + +#[action] +async fn get_post(Path(params): Path) -> Text { + Text::new(format!("User {} Post {}", params.id, params.post_id)) +} +``` + +## Catch-All Segments + +Use `{*rest}` for catch-all routes that match any remaining path: + +```rust +// Route: /files/{*path} +// Matches: /files/docs/readme.md -> path = "docs/readme.md" + +#[action] +async fn serve_file(Path(path): Path) -> Text { + Text::new(format!("Serving: {}", path)) +} +``` + +## HTTP Methods + +Specify allowed methods in your route definition: + +```toml +[[triggers.http]] +path = "/resource" +methods = ["GET", "POST", "PUT", "DELETE"] +handler = "my_app_core::handlers::resource" +``` + +Or programmatically: + +```rust +router + .route(Method::GET, "/resource", get_resource) + .route(Method::POST, "/resource", create_resource) + .route(Method::PUT, "/resource/{id}", update_resource) + .route(Method::DELETE, "/resource/{id}", delete_resource) +``` + +EdgeZero automatically returns `405 Method Not Allowed` for requests that match a path but use an unsupported method. + +## Route Listing + +Enable route listing for debugging: + +```rust +let router = RouterService::builder() + .enable_route_listing() + .route(Method::GET, "/hello", hello) + .build(); +``` + +This exposes a JSON endpoint at `/__edgezero/routes`: + +```json +[ + { "method": "GET", "path": "/hello" }, + { "method": "GET", "path": "/__edgezero/routes" } +] +``` + +Customize the listing path: + +```rust +RouterService::builder() + .enable_route_listing_at("/debug/routes") +``` + +## Path Syntax + +EdgeZero uses matchit's path syntax: + +| Pattern | Example | Matches | +|---------|---------|---------| +| `/static` | `/static` | Exact match only | +| `/{param}` | `/users/{id}` | Single segment: `/users/123` | +| `/{*catch}` | `/files/{*path}` | Rest of path: `/files/a/b/c` | + +::: warning Legacy Syntax +Axum-style `:name` parameters are **not supported**. Use `{name}` instead. +::: + +## Route Priority + +Routes are matched in registration order. More specific routes should be registered before catch-alls: + +```rust +// Good: specific route first +router + .route(Method::GET, "/users/me", get_current_user) + .route(Method::GET, "/users/{id}", get_user_by_id) + +// Bad: catch-all shadows specific routes +router + .route(Method::GET, "/users/{id}", get_user_by_id) + .route(Method::GET, "/users/me", get_current_user) // Never reached! +``` + +## Next Steps + +- Learn about [Handlers & Extractors](/guide/handlers) for processing requests +- Explore [Middleware](/guide/middleware) for cross-cutting concerns diff --git a/docs/guide/streaming.md b/docs/guide/streaming.md new file mode 100644 index 0000000..c4523ac --- /dev/null +++ b/docs/guide/streaming.md @@ -0,0 +1,132 @@ +# Streaming + +EdgeZero supports streaming responses for large payloads, real-time data, and server-sent events. + +## Streaming Responses + +Use `Body::stream` to yield response chunks progressively: + +```rust +use edgezero_core::action; +use edgezero_core::body::Body; +use edgezero_core::http::Response; +use futures::stream; + +#[action] +async fn stream_data() -> Response { + let chunks = vec![ + Ok::<_, std::io::Error>(vec![b'H', b'e', b'l', b'l', b'o']), + Ok(vec![b' ']), + Ok(vec![b'W', b'o', b'r', b'l', b'd']), + ]; + + let body = Body::stream(stream::iter(chunks)); + + Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(body) + .unwrap() +} +``` + +## How Streaming Works + +The router keeps streams intact through the adapter layer: + +1. Your handler returns `Body::stream(...)` with a `Stream` of chunks +2. The adapter writes chunks sequentially to the provider's output API +3. Fastly uses `stream_to_client`, Cloudflare uses `ReadableStream` +4. The client receives data as it becomes available + +## Server-Sent Events + +Stream events to clients with SSE: + +```rust +use futures::stream::StreamExt; + +#[action] +async fn events() -> Response { + let events = async_stream::stream! { + for i in 0..10 { + yield Ok::<_, std::io::Error>( + format!("data: Event {}\n\n", i).into_bytes() + ); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }; + + Response::builder() + .status(200) + .header("content-type", "text/event-stream") + .header("cache-control", "no-cache") + .body(Body::stream(events)) + .unwrap() +} +``` + +## Body Modes + +Routes can specify their body handling mode in the manifest: + +```toml +[[triggers.http]] +path = "/upload" +methods = ["POST"] +handler = "my_app::handlers::upload" +body-mode = "buffered" # or "stream" +``` + +| Mode | Behavior | +|------|----------| +| `buffered` | Body is fully read into memory before handler runs | +| `stream` | Body is passed as a stream for progressive processing | + +## Transparent Decompression + +EdgeZero automatically decompresses gzip and brotli responses from upstream services: + +```rust +// Proxied response with Content-Encoding: gzip is automatically decoded +let response = proxy.forward(request).await?; +// response.body is now decompressed +``` + +This happens transparently in the adapter layer using shared decoders from `edgezero-core`. + +## Memory Considerations + +Streaming is essential for: + +- Large file downloads +- Video/audio content +- Real-time data feeds +- Responses larger than available memory + +::: warning Platform Limits +Edge platforms have memory constraints. A Fastly Compute instance has ~128MB by default. Always stream large responses rather than buffering. +::: + +## Chunked Transfer + +When the response size is unknown, EdgeZero uses chunked transfer encoding: + +```rust +#[action] +async fn dynamic_content() -> Response { + let stream = generate_content_stream(); + + // No Content-Length header needed + Response::builder() + .status(200) + .header("content-type", "application/octet-stream") + .body(Body::stream(stream)) + .unwrap() +} +``` + +## Next Steps + +- Learn about [Proxying](/guide/proxying) for forwarding requests upstream +- Explore adapter-specific streaming in [Fastly](/guide/adapters/fastly) and [Cloudflare](/guide/adapters/cloudflare) guides diff --git a/docs/guide/what-is-edgezero.md b/docs/guide/what-is-edgezero.md new file mode 100644 index 0000000..38f7949 --- /dev/null +++ b/docs/guide/what-is-edgezero.md @@ -0,0 +1,47 @@ +# What is EdgeZero? + +EdgeZero is a production-ready toolkit for writing an HTTP workload once and deploying it across multiple edge providers. The core stays runtime-agnostic so it compiles cleanly to WebAssembly targets (Fastly Compute@Edge, Cloudflare Workers) and to native hosts (Axum/Tokio) without code changes. + +## Key Features + +EdgeZero provides developers with: + +- **Portable HTTP workloads** - Write your business logic once using the shared `edgezero-core` primitives, then compile to any supported target +- **Multiple deployment targets** - Deploy to Fastly Compute@Edge, Cloudflare Workers, or native Axum servers from the same codebase +- **Type-safe extractors** - Use ergonomic extractors like `Json`, `Path`, and `ValidatedQuery` for clean handler code +- **Streaming support** - Stream responses progressively with `Body::stream` for long-lived or chunked responses +- **Proxy helpers** - Forward traffic upstream with built-in `ProxyRequest` and `ProxyService` abstractions +- **CLI tooling** - Scaffold projects, run dev servers, and deploy with the `edgezero` CLI + +## How It Works + +EdgeZero separates your application into layers: + +1. **Core logic** - Your handlers and business logic live in a shared crate that depends only on `edgezero-core` +2. **Adapters** - Thin bridge crates translate provider-specific request/response types into the portable model +3. **Entrypoints** - Minimal main functions that wire the adapter to your core app + +This architecture means you can: +- Develop locally with the Axum adapter's dev server +- Test your handlers in isolation without provider SDKs +- Deploy the same logic to multiple edge platforms + +## Supported Platforms + +| Platform | Target | Status | +|----------|--------|--------| +| Fastly Compute@Edge | `wasm32-wasip1` | Stable | +| Cloudflare Workers | `wasm32-unknown-unknown` | Stable | +| Axum/Tokio (native) | Native host | Stable | + +## Use Cases + +- **Edge APIs** - Low-latency JSON APIs running close to users +- **Proxy services** - Request forwarding with header manipulation +- **A/B testing** - Edge-side traffic splitting and experimentation +- **Content transformation** - HTML/CSS rewriting at the edge +- **Multi-cloud deployment** - Avoid vendor lock-in by targeting multiple providers + +## Next Steps + +Continue to [Getting Started](/guide/getting-started) to set up your first EdgeZero project. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..8e3b8bb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,29 @@ +--- +layout: home + +hero: + name: "EdgeZero" + text: "Write Once, Deploy Everywhere" + tagline: "Production-ready toolkit for portable edge HTTP workloads" + actions: + - theme: brand + text: Get Started + link: /guide/getting-started + - theme: alt + text: View on GitHub + link: https://github.com/stackpop/edgezero + +features: + - title: Write Once, Deploy Anywhere + details: Build your HTTP workload once with runtime-agnostic core code that compiles to WebAssembly or native targets without changes. + - title: Fastly Compute Support + details: Deploy to Fastly Compute@Edge with zero-cold-start WASM binaries using the wasm32-wasip1 target. + - title: Cloudflare Workers Support + details: Run on Cloudflare Workers with seamless wrangler integration and wasm32-unknown-unknown compilation. + - title: Native Development (Axum) + details: Develop locally with a full-featured Axum/Tokio dev server, then deploy to containers or native hosts. + - title: Type-Safe Extractors + details: Use ergonomic extractors like Json, Path, and ValidatedQuery with the #[action] macro for clean handler code. + - title: Streaming & Proxying + details: Stream responses progressively with Body::stream and forward traffic upstream with built-in proxy helpers. +--- diff --git a/docs/manifest.md b/docs/manifest.md deleted file mode 100644 index 3ead92f..0000000 --- a/docs/manifest.md +++ /dev/null @@ -1,197 +0,0 @@ -# edgezero.toml Manifest - -The `edgezero.toml` file describes an EdgeZero application, mirroring the -ergonomics of Spin's manifest while remaining provider agnostic. New workspaces -scaffolded with `edgezero new` now include this manifest by default. - -## Top-level structure - -```toml -[app] -name = "demo" -version = "0.1.0" -kind = "http" -entry = "crates/demo-core" -middleware = ["edgezero_core::middleware::RequestLogger"] - -[[triggers.http]] -id = "root" -path = "/" -methods = ["GET"] -handler = "demo_core::handlers::root" -adapters = ["fastly", "cloudflare"] -body-mode = "buffered" - -[environment] - -[[environment.variables]] -name = "API_BASE_URL" -env = "API_BASE_URL" -value = "https://example.com/api" - -[[environment.secrets]] -name = "API_TOKEN" -adapters = ["fastly", "cloudflare"] -env = "API_TOKEN" - -[adapters.fastly.adapter] -crate = "crates/demo-adapter-fastly" -manifest = "crates/demo-adapter-fastly/fastly.toml" - -[adapters.fastly.build] -target = "wasm32-wasip1" -profile = "release" - -[adapters.fastly.commands] -build = "cargo build --release --target wasm32-wasip1 -p demo-adapter-fastly" -serve = "fastly compute serve -C crates/demo-adapter-fastly" -deploy = "fastly compute deploy -C crates/demo-adapter-fastly" - -[adapters.fastly.logging] -endpoint = "stdout" -level = "info" -echo_stdout = true - - -[adapters.cloudflare.adapter] -crate = "crates/demo-adapter-cloudflare" -manifest = "crates/demo-adapter-cloudflare/wrangler.toml" - -[adapters.cloudflare.build] -target = "wasm32-unknown-unknown" -profile = "release" - -[adapters.cloudflare.commands] -build = "cargo build --release --target wasm32-unknown-unknown -p demo-adapter-cloudflare" -serve = "wrangler dev --config crates/demo-adapter-cloudflare/wrangler.toml" -deploy = "wrangler publish --config crates/demo-adapter-cloudflare/wrangler.toml" - -[adapters.cloudflare.logging] -level = "info" -``` - -### `[app]` - -Metadata about the application: the display name, the crate that exposes the router (`entry`), -and an optional `middleware = ["path::to::Middleware"]` list of zero-argument constructors -that are registered before routes are added. Each entry must resolve to a type or function -implementing `edgezero_core::middleware::Middleware`, letting global behaviour (logging, CORS, auth guards, -etc.) live alongside the manifest instead of being hard-wired in Rust. - -### `app.middleware` - -Manifest-driven equivalent of `RouterService::builder().middleware(...)`. Middleware are applied -in order before the request is handed to route handlers. For example: - -```toml -[app] -name = "app-demo" -entry = "crates/app-demo-core" -middleware = [ - "edgezero_core::middleware::RequestLogger", - "app_demo_core::cors::Cors" -] -``` - -Each item must be publicly accessible and expose a unit struct or zero-argument constructor that -implements `Middleware`. - -### `[[triggers.http]]` - -Defines HTTP routes and their handlers. Fields: - -- `id`: Stable identifier for the route (optional but useful for tooling). -- `path`: URI template understood by `edgezero-core`. -- `methods`: Allowed HTTP methods (defaults to `GET` if omitted). -- `handler`: Path to the handler function (for reference/documentation). -- `adapters`: Which adapters expose the route. Empty means “all adapters”. -- `body-mode`: Either `buffered` or `stream` to document expected behaviour. - -### `[environment]` - -Declares environment variables and secrets shared across adapters. Each entry -supports a human-friendly description, the upstream environment key (`env`, -defaulting to the `name`), an optional default `value`, and a provider filter. -When running provider commands through `edgezero-cli`, variables with a default -`value` are injected into the child process and secrets must already be present -in the environment; missing secrets will cause the command to abort with a -helpful error message. - -### `[adapters.]` - -Describes how a provider adapter is built and invoked. - -- `[adapters..adapter]`: Points at the adapter crate and any provider - manifest (e.g. `fastly.toml`, `wrangler.toml`). -- `[adapters..build]`: Build target, profile, and optional feature list. -- `[adapters..commands]`: Convenience commands for build/serve/deploy. - -The EdgeZero CLI will, when present, run these commands for `build`, `serve`, -and `deploy` before falling back to the adapter's built-in behaviour. That lets -you customise provider tooling (e.g. add flags) without recompiling the CLI. - -### `[adapters..logging]` - -Optional logging configuration nested under each adapter. Current fields: - -- `endpoint` (Fastly only): Name passed to `init_logger` (defaults to `stdout`). -- `level`: Log level (`trace`, `debug`, `info`, `warn`, `error`, `off`). Defaults to `info`. -- `echo_stdout` (Fastly only): Whether to mirror logs to stdout. Defaults to `true`. - -The Fastly adapter in the demo looks these values up before installing its -logger, and the CLI scaffolding emits the same pattern for new projects. Other -adapters can obtain provider-specific settings via -`Manifest::logging_or_default("provider")`, which guarantees a concrete log -level while leaving optional values available for provider-specific defaults at -runtime. - -Manifest parsing lives in `edgezero-core::manifest`, and CLI commands now verify -that a provider is declared before invoking adapter-specific tooling. Additional -provider metadata (extra environment bindings, secrets per provider, extra -commands) can be layered under these sections without breaking existing tooling -thanks to permissive deserialisation defaults. - -`ManifestLoader` validates basic manifest constraints (non-empty trigger paths -and handlers, well-formed logging levels, etc.) so mistakes are caught early at -startup or during macro expansion. - -`edgezero-core::ManifestLoader` provides a shared parser so applications can load -the manifest at runtime. The demo app uses this loader to build its router from -the manifest, and the CLI reuses the same types when executing provider -commands. - -## CLI integration - -`edgezero build|serve|deploy --adapter ` looks up the provider entry in -`edgezero.toml`. If a `[adapters..commands]` block supplies a `build`, -`serve`, or `deploy` command, the CLI executes it from the manifest directory. -This allows each adapter to decide how artifacts are produced (for example, -invoking `cargo build --target wasm32-wasip1` for Fastly or `wrangler dev` for -Cloudflare). When commands are omitted, the CLI falls back to the built-in -helpers shipped with the adapters (currently the Fastly adapter). - -The example app under `examples/app-demo` ships an `edgezero.toml` manifest that -drives both runtime routing and CLI commands. `app-demo-core` reads the manifest -at startup to register HTTP routes (rather than hard-coding paths in Rust), and -running `edgezero build --adapter fastly` from the workspace root invokes the -Fastly build command specified in the manifest. - -## Generating routers via macro - -Use `edgezero_core::app!("path/to/edgezero.toml", AppName);` inside your -crate to generate a `Hooks` implementation and `build_router` function directly -from the manifest. The `AppName` argument is optional; when omitted the macro -emits a struct named `App`. The macro understands the HTTP trigger list (including -methods and handler paths) and emits the wiring automatically. It also accepts -crate-qualified handler paths such as `app_demo_core::handlers::root`, rewriting -them to the local `crate::…` form the compiler expects. The demo app’s `lib.rs` -shows the minimal usage: - -```rust -mod handlers; - -edgezero_core::app!("../../edgezero.toml"); -``` - -Handlers referenced in the manifest can therefore use either `crate::` or the -crate name prefix; both get normalised during macro expansion. diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..995f729 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2514 @@ +{ + "name": "edgezero-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "edgezero-docs", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "vitepress": "^1.5.0" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.13.0.tgz", + "integrity": "sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.47.0.tgz", + "integrity": "sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.47.0.tgz", + "integrity": "sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.47.0.tgz", + "integrity": "sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.47.0.tgz", + "integrity": "sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.47.0.tgz", + "integrity": "sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.47.0.tgz", + "integrity": "sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.47.0.tgz", + "integrity": "sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.47.0.tgz", + "integrity": "sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.47.0.tgz", + "integrity": "sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.47.0.tgz", + "integrity": "sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.47.0.tgz", + "integrity": "sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.47.0.tgz", + "integrity": "sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.47.0.tgz", + "integrity": "sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.68", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.68.tgz", + "integrity": "sha512-bQPl1zuZlX6AnofreA1v7J+hoPncrFMppqGboe/SH54jZO37meiBUGBqNOxEpc0HKfZGxJaVVJwZd4gdMYu3hw==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.47.0.tgz", + "integrity": "sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.13.0", + "@algolia/client-abtesting": "5.47.0", + "@algolia/client-analytics": "5.47.0", + "@algolia/client-common": "5.47.0", + "@algolia/client-insights": "5.47.0", + "@algolia/client-personalization": "5.47.0", + "@algolia/client-query-suggestions": "5.47.0", + "@algolia/client-search": "5.47.0", + "@algolia/ingestion": "1.47.0", + "@algolia/monitoring": "1.47.0", + "@algolia/recommend": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "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/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==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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-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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..e179898 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,15 @@ +{ + "name": "edgezero-docs", + "version": "1.0.0", + "description": "Documentation site for EdgeZero", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^1.5.0" + } +} From 60a42c2a4547dc8c458e1fb2919456310ebcd27c Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:24:50 -0800 Subject: [PATCH 2/3] Updated with linting configuration --- .github/dependabot.yml | 6 +- .github/workflows/deploy-docs.yml | 4 +- .github/workflows/format.yml | 35 +- .github/workflows/test.yml | 2 +- .gitignore | 1 + .tool-versions | 3 +- README.md | 212 +---- TODO.md | 61 ++ docs/.prettierignore | 3 + docs/.prettierrc | 7 + docs/.vitepress/config.mts | 38 +- docs/eslint.config.js | 20 + docs/guide/adapters/axum.md | 109 +-- docs/guide/adapters/cloudflare.md | 105 +-- docs/guide/adapters/fastly.md | 110 +-- docs/guide/adapters/overview.md | 22 +- docs/guide/architecture.md | 18 +- docs/guide/cli-reference.md | 87 +- docs/guide/configuration.md | 122 +-- docs/guide/getting-started.md | 8 +- docs/guide/handlers.md | 204 +++- docs/guide/middleware.md | 120 ++- docs/guide/proxying.md | 249 +---- docs/guide/roadmap.md | 39 + docs/guide/routing.md | 57 +- docs/guide/streaming.md | 40 +- docs/guide/what-is-edgezero.md | 11 +- docs/index.md | 10 +- docs/package-lock.json | 1467 ++++++++++++++++++++++++++++- docs/package.json | 11 +- 30 files changed, 2252 insertions(+), 929 deletions(-) create mode 100644 docs/.prettierignore create mode 100644 docs/.prettierrc create mode 100644 docs/eslint.config.js create mode 100644 docs/guide/roadmap.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 984d281..e78d9b3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "cargo" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: 'cargo' # See documentation for possible values + directory: '/' # Location of package manifests schedule: - interval: "weekly" + interval: 'weekly' diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 68ceed5..9c40e95 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -6,7 +6,7 @@ on: paths: - 'docs/**' - '.github/workflows/deploy-docs.yml' - workflow_dispatch: # Allow manual triggers + workflow_dispatch: # Allow manual triggers # Sets permissions for GitHub Pages deployment permissions: @@ -26,7 +26,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # For lastUpdated feature + fetch-depth: 0 # For lastUpdated feature - name: Retrieve Node.js version id: node-version diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 1d7b067..8d3a2ec 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,4 +1,4 @@ -name: "Run Format" +name: 'Run Format' on: push: @@ -38,7 +38,7 @@ jobs: - name: Set up rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: - components: "clippy, rustfmt" + components: 'clippy, rustfmt' toolchain: ${{ steps.rust-version.outputs.rust-version }} - name: Fetch dependencies (locked) @@ -49,3 +49,34 @@ jobs: - name: Run cargo clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + format-docs: + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs + + steps: + - uses: actions/checkout@v4 + + - name: Retrieve Node.js version + id: node-version + working-directory: . + run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + shell: bash + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ steps.node-version.outputs.node-version }} + cache: 'npm' + cache-dependency-path: docs/package.json + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Run Prettier (check) + run: npm run format diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ecb2ca5..85ef9e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: "Run Tests" +name: 'Run Tests' on: push: diff --git a/.gitignore b/.gitignore index b99f0fe..1ddbadc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ target/ .DS_Store # IDE - VSCode +.claude/ .vscode/* !.vscode/settings.json !.vscode/tasks.json diff --git a/.tool-versions b/.tool-versions index dbf830f..9934717 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ fasltly v13.0.0 -rust 1.90.0 +nodejs 24.12.0 +rust 1.91.1 diff --git a/README.md b/README.md index 459d56f..bd79dd0 100644 --- a/README.md +++ b/README.md @@ -1,211 +1,41 @@ # EdgeZero -EdgeZero is a production-ready toolkit for writing an HTTP workload once and -deploying it across multiple edge providers. The core stays runtime-agnostic so -it compiles cleanly to WebAssembly targets (Fastly Compute@Edge, Cloudflare -Workers) and to native hosts (Axum/Tokio) without code changes. +Production-ready toolkit for portable edge HTTP workloads. Write once, deploy to Fastly Compute, Cloudflare Workers, or native Axum servers. -## Workspace layout - -- `crates/edgezero-core` - routing, request/response primitives, middleware chaining, extractor utilities built on `http`, `tower::Service`, and `matchit` 0.8. Handlers opt into extractors such as `Json`, `Path`, and `ValidatedQuery`, and the crate re-exports HTTP types (`Method`, `StatusCode`, `HeaderMap`, ...). -- `crates/edgezero-macros` - procedural macros that power `#[edgezero_core::action]` and related derive helpers. -- `crates/edgezero-adapter-fastly` - Fastly Compute@Edge bridge that maps Fastly request/response types into the shared model and exposes `FastlyRequestContext` plus logging conveniences. -- `crates/edgezero-adapter-cloudflare` - Cloudflare Workers bridge providing `CloudflareRequestContext` and logger bootstrap helpers. -- `crates/edgezero-adapter-axum` - host-side adapter that wraps `RouterService` in Axum/Tokio services for local development and native deployments (the dev server now runs through this crate). -- `crates/edgezero-cli` - CLI for project scaffolding, the local dev server, and adapter-aware build/deploy helpers. Ships with an optional demo dependency. -- `examples/app-demo` - reference application built on the shared router. Includes `crates/app-demo-core` (routes), `crates/app-demo-adapter-fastly` (Fastly binary + `fastly.toml`), `crates/app-demo-adapter-cloudflare` (Workers entrypoint + `wrangler.toml`), and `crates/app-demo-adapter-axum` (native dev server). - -## Quick start +## Quick Start ```bash -# Install the CLI (from this workspace or a published crate) +# Install the CLI cargo install --path crates/edgezero-cli -# Scaffold a new EdgeZero app targeting Fastly, Cloudflare, and Axum +# Create a new project edgezero new my-app --adapters fastly cloudflare axum cd my-app -# Start the local Axum-powered dev server +# Start the dev server edgezero dev -# Hit one of the generated endpoints -curl http://127.0.0.1:8787/echo/alice - -# Run your workspace tests -cargo test - -# Optional: explore the demo project bundled with this repo -cargo run -p edgezero-cli --features dev-example -- dev +# Test it +curl http://127.0.0.1:8787/ ``` -To run the demo router from `examples/app-demo`, enable the optional -`dev-example` feature as shown above. Without that feature the CLI always loads -the manifest in your current project directory. - -The demo routes showcase core features: - -- `/` - static response to verify the app is running. -- `/echo/{name}` - path parameter extraction. -- `/headers` - direct `RequestContext` access. -- `/stream` - streaming bodies via `Body::stream`. -- `POST /echo` - JSON extractor + response builder. -- `/info` - shared state injection. - -Handlers stay concise by using the `#[action]` macro re-exported from `edgezero-core`: - -```rust -use edgezero_core::action; -use edgezero_core::extractor::Json; -use edgezero_core::response::Text; - -#[derive(serde::Deserialize)] -struct EchoBody { - name: String, -} - -#[action] -async fn echo_json(Json(body): Json) -> Text { - Text::new(format!("Hello, {}!", body.name)) -} -``` - -## CLI tooling - -The CLI and adapters now expect an `edgezero.toml` manifest alongside your workspace. The manifest -describes the shared app (entry crate, optional middleware list, routes, adapters, and logging) -so Fastly/Cloudflare binaries, the CLI, and any local tooling all agree on configuration. The demo -manifest lives in `examples/app-demo/edgezero.toml`, and the scaffolder emits the same structure for -new projects. +## Documentation -The `edgezero-cli` crate produces the `edgezero` binary (enabled by the `cli` feature). Run it locally with `cargo run -p edgezero-cli -- `. Key subcommands: +Full documentation is available at **[stackpop.github.io/edgezero](https://stackpop.github.io/edgezero/)**. -- `edgezero new` - scaffolds a fully wired workspace (pass `--adapters` to pick your targets). -- `edgezero dev` - starts the local Axum HTTP server using the current project's manifest (pass `--features dev-example` when running from this repository to boot the demo app). -- `edgezero build --adapter fastly` - builds the Fastly example to `wasm32-wasip1` and copies the artifact into `edgezero/pkg/`. -- `edgezero serve --adapter fastly` - shells out to `fastly compute serve` after locating the Fastly manifest. -- `edgezero deploy --adapter fastly` - wraps `fastly compute deploy`. -- `edgezero build --adapter axum` - builds your native entrypoint (useful for containers or local integration tests). -- `edgezero serve --adapter axum` - runs the generated Axum entrypoint with `cargo run`, ideal for local or containerised development. +- [Getting Started](https://stackpop.github.io/edgezero/guide/getting-started) - Project setup and first steps +- [Architecture](https://stackpop.github.io/edgezero/guide/architecture) - How EdgeZero works +- [Configuration](https://stackpop.github.io/edgezero/guide/configuration) - `edgezero.toml` reference +- [CLI Reference](https://stackpop.github.io/edgezero/guide/cli-reference) - All CLI commands -Adapters register themselves lazily through their `edgezero_adapter_*::cli` modules. With the Axum adapter available you can generate, serve, and test a native host target without leaving the workspace. +## Supported Platforms -## Logging - -`edgezero-core` relies on the standard `log` facade. Platform adapters expose helper -functions so you can install the right backend when your app boots: - -- Fastly: call `edgezero_adapter_fastly::init_logger()` (wraps `log_fastly`). -- Cloudflare Workers: call `edgezero_adapter_cloudflare::init_logger()` (logs via - Workers `console_log!`). -- Axum/native: install a standard logger (`simple_logger`, `tracing-subscriber`, etc.) before booting `edgezero_adapter_axum::AxumDevServer`. -- Other targets: initialise a fallback logger such as `simple_logger` before building - your app. - -The helper `run_app::(include_str!("path/to/edgezero.toml"), req)` in -`edgezero-adapter-fastly` and the Cloudflare equivalent encapsulate manifest loading and logger -initialisation, so the adapters you scaffold only need to call the helper from `main`. - -## Provider builds - -Fastly Compute@Edge (requires the `fastly` CLI and the `wasm32-wasip1` target): - -```bash -rustup target add wasm32-wasip1 -cd edgezero/examples/app-demo -cargo build -p app-demo-adapter-fastly --target wasm32-wasip1 --features fastly -# or from the workspace root: -cargo run -p edgezero-cli -- build --adapter fastly -cargo run -p edgezero-cli -- serve --adapter fastly -``` - -The CLI helpers locate `fastly.toml`, build the Wasm artifact, place it in `edgezero/pkg/`, and run `fastly compute serve` from `examples/app-demo/crates/app-demo-adapter-fastly`. - -Cloudflare Workers (requires `wrangler` and the `wasm32-unknown-unknown` target): - -```bash -rustup target add wasm32-unknown-unknown -cd edgezero/examples/app-demo -cargo build -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown -wrangler dev --config crates/app-demo-adapter-cloudflare/wrangler.toml -``` +| Platform | Target | Status | +| ------------------ | ------------------------ | ------ | +| Fastly Compute | `wasm32-wasip1` | Stable | +| Cloudflare Workers | `wasm32-unknown-unknown` | Stable | +| Axum (Native) | Host | Stable | -Axum / native hosts: - -```bash -# Build or run using the scaffolded commands -edgezero build --adapter axum -edgezero serve --adapter axum -``` - -The Fastly and Cloudflare adapters translate provider request/response shapes into the shared `edgezero-core` model and stash provider metadata in the request extensions so handlers can reach runtime-specific APIs. - -## Path parameters - -`edgezero-core` uses matchit 0.8+. Define parameters with `{name}` segments -(`/blog/{slug}`) and catch-alls with `{*rest}`. Legacy Axum-style `:name` -segments are intentionally unsupported. - -## Route listing - -Enable `RouterBuilder::enable_route_listing()` when you want a quick view of the -registered routes. It injects a JSON endpoint at -`DEFAULT_ROUTE_LISTING_PATH` (defaults to `/__edgezero/routes`) that returns an -array of `{ "method": "GET", "path": "/..." }` entries for every handler. -Use `RouterBuilder::enable_route_listing_at("/debug/routes")` to expose the -listing at a custom path. - -## Streaming responses - -Handlers can return `Body::stream` to yield response chunks progressively. The -router keeps the stream intact all the way to the adapters; the Fastly and -Cloudflare bridges buffer chunks sequentially while writing to the provider -runtime APIs, so long-lived streams remain compatible with Wasm targets. Responses compressed with gzip or brotli are transparently -decoded before they reach handlers so you can reformat or transform the -payload before sending it downstream. - -## Proxying upstream services - -`edgezero-core` ships with `ProxyRequest`, `ProxyResponse`, and the `ProxyService` wrapper so edge adapters can forward traffic while reusing the same handler logic: - -```rust -use edgezero_core::http::Uri; -use edgezero_core::proxy::{ProxyRequest, ProxyService}; - -let target: Uri = "https://example.com/api".parse()?; -let proxy_request = ProxyRequest::from_request(request, target); -let response = ProxyService::new(client).forward(proxy_request).await?; -``` - -Use the adapter-specific clients (`edgezero_adapter_fastly::FastlyProxyClient` and `edgezero_adapter_cloudflare::CloudflareProxyClient`) when compiling for those adapters, and swap in lightweight test clients during unit tests. The proxy helpers preserve streaming bodies and transparently decode gzip or brotli payloads before they reach your handler. - -## Testing - -Unit tests live next to the modules they exercise. Run the entire suite with -`cargo test`, or scope to a single crate via `cargo test -p edgezero-core`. -The adapter crates include lightweight host-side tests that validate context -insertion and URI parsing without needing the Wasm toolchains. - -### Wasm runners (Fastly / WASI) - -Some adapter tests target `wasm32-wasip1`; Cargo needs a Wasm runtime to execute -the generated binaries. Install the following tools before running those tests: - -- Wasmtime (executes `wasm32-wasip1` tests) - - macOS: `brew install wasmtime` - - Linux: `curl https://wasmtime.dev/install.sh -sSf | bash` - - Windows: follow for the MSI/winget installers -- Viceroy (Fastly’s local Compute@Edge simulator) - - macOS: `brew install fastly/tap/viceroy` - - Linux & Windows: download the latest release archive from - , extract it, and place the - binary on your `PATH` - -Tell Cargo to use Wasmtime when running the wasm tests: - -```bash -export CARGO_TARGET_WASM32_WASIP1_RUNNER="wasmtime run --dir=." -cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 -``` +## License -Streaming responses are covered in the `edgezero-core` router tests and in the -Fastly adapter tests to ensure chunked bodies make it to the provider output. +MIT diff --git a/TODO.md b/TODO.md index 700e47b..df4b60f 100644 --- a/TODO.md +++ b/TODO.md @@ -88,6 +88,7 @@ High-level backlog and decisions to drive the next milestones. - [ ] Provider additions: prototype a third adapter (e.g. AWS Lambda@Edge or Vercel Edge Functions) using the stabilized adapter API to validate cross-provider abstractions. - [x] Manifest ergonomics: design an `edgezero.toml` schema that mirrors Spin’s manifest convenience (route triggers, env/secrets, build targets) while remaining provider-agnostic; update CLI scaffolding accordingly. (`crates/edgezero-cli/src/manifest.rs`, templates in `crates/edgezero-cli/src/templates/root/edgezero.toml.hbs`, doc `docs/manifest.md`, app-demo manifest `examples/app-demo/edgezero.toml`) - [ ] Tooling parity: extend `edgezero-cli` with template/plugin style commands (similar to Spin templates) to streamline new app scaffolds and provider-specific wiring. +- [ ] CLI parity backlog: add `edgezero --list-adapters`, standardize exit codes, search up for `edgezero.toml`, respect `RUST_LOG` for dev output, and bake in hot reload for `edgezero dev`. ## Open Design Questions (for later pickup) @@ -488,3 +489,63 @@ High-level backlog and decisions to drive the next milestones. - Summary: Extended `#[action]` to accept a single `RequestContext` parameter (with duplicate detection and helper tests) and applied the attribute to `proxy_demo`, relying on the original handler logic. - Assumptions: `RequestContext` parameters are owned values (no reference variants needed) and only one is expected per handler. - Outstanding: None; `cargo test` across the workspace passed after the macro test adjustment. + +## Codex Plan (2026-01-27 - Documentation Exposure Review) + +- [x] Diff `main` vs current branch to scope doc-related changes. +- [x] Review updated/new docs for redundancy, inconsistency, missing coverage, and verbosity. +- [x] Check overall documentation thoroughness and note gaps vs the framework surface area. +- [x] Compile review findings with file references and prioritized severity. + +## Review (2026-01-27 00:20:19 UTC) + +- Summary: Compared `main` against the `feature/vitepress` branch, audited the new VitePress guide set plus CLI/adapter docs, and assembled a prioritized findings list focused on inaccuracies, redundancy, missing coverage, and verbosity. +- Assumptions: Documentation review targets current source behavior (not proposed future APIs); adapter contract expectations should reflect existing Fastly/Cloudflare/Axum implementations unless explicitly labeled aspirational. +- Outstanding: None (review-only; no code changes applied). + +## Review (2026-01-27 00:50:51 UTC) + +- Summary: Aligned guide content with current EdgeZero APIs (App::build_app, adapter entrypoints, middleware signature, proxy clients), corrected routing/streaming/handlers/CLI details, and refreshed manifest/logging docs; added the missing CLI/dev features list to the roadmap. +- Assumptions: Docs now reflect current behavior for Fastly/Cloudflare/Axum adapters and the CLI; future features are captured explicitly in the roadmap rather than implied in guides. +- Outstanding: None (docs-only updates). + +## Review (2026-01-27 01:02:05 UTC) + +- Summary: Condensed the proxying guide into a single end-to-end example that uses adapter proxy handles, and added short logging-status callouts to the Fastly, Cloudflare, and Axum adapter docs. +- Assumptions: The proxy handle approach is the preferred public pattern; adapter logging notes should stay concise and match current defaults. +- Outstanding: None (docs-only updates). + +## Review (2026-01-27 01:05:18 UTC) + +- Summary: Added a dedicated `docs/guide/roadmap.md` page containing the current roadmap and design questions, and linked it in the VitePress sidebar. +- Assumptions: The roadmap page mirrors `TODO.md` and is a public-facing summary of ongoing planning work. +- Outstanding: None (docs-only updates). + +## Review (2026-01-27 01:09:04 UTC) + +- Summary: Expanded the roadmap page with doc/CLI alignment gaps found during review and added an explicit Spin support item. +- Assumptions: The new roadmap bullets are directional and do not imply implementation order. +- Outstanding: None (docs-only updates). + +## Codex Plan (2026-01-27 - Roadmap Doc Page) + +- [x] Add a dedicated roadmap page under `docs/guide/roadmap.md`. +- [x] Populate it with the roadmap content (including the CLI parity backlog list). +- [x] Wire the roadmap page into the VitePress sidebar/navigation. + +## Codex Plan (2026-01-27 - Roadmap Findings + Spin Support) + +- [x] Add the key doc/CLI gaps found during the review to the roadmap page. +- [x] Add an explicit roadmap item for Spin support (define scope at the doc level). + +## Codex Plan (2026-01-27 - Proxying Snippet + Adapter Logging Callout) + +- [x] Condense proxying guide into a single end-to-end example using adapter proxy handles. +- [x] Add a short logging status callout to the adapter docs (Axum/Cloudflare/Fastly). + +## Codex Plan (2026-01-27 - Docs Alignment + Roadmap Additions) + +- [x] Update guides to reflect current APIs (App::build_app, adapter entrypoints, middleware signature, proxy client usage). +- [x] Correct routing, streaming, handlers, and CLI reference docs to match current behavior. +- [x] Refresh configuration docs to align with manifest schema and loader APIs. +- [x] Add missing-feature backlog (list-adapters, exit codes, manifest search-up, RUST_LOG, hot reload) to the roadmap section. diff --git a/docs/.prettierignore b/docs/.prettierignore new file mode 100644 index 0000000..94aa6e0 --- /dev/null +++ b/docs/.prettierignore @@ -0,0 +1,3 @@ +.vitepress/cache +.vitepress/dist +node_modules diff --git a/docs/.prettierrc b/docs/.prettierrc new file mode 100644 index 0000000..364896f --- /dev/null +++ b/docs/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "proseWrap": "preserve" +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 8f4fb25..2133fef 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -2,9 +2,9 @@ import { defineConfig } from 'vitepress' // https://vitepress.dev/reference/site-config export default defineConfig({ - title: "EdgeZero", - description: "Production-ready toolkit for portable edge HTTP workloads", - base: "/edgezero/", + title: 'EdgeZero', + description: 'Production-ready toolkit for portable edge HTTP workloads', + base: '/edgezero/', themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ @@ -20,8 +20,9 @@ export default defineConfig({ items: [ { text: 'What is EdgeZero?', link: '/guide/what-is-edgezero' }, { text: 'Getting Started', link: '/guide/getting-started' }, - { text: 'Architecture', link: '/guide/architecture' } - ] + { text: 'Architecture', link: '/guide/architecture' }, + { text: 'Roadmap', link: '/guide/roadmap' }, + ], }, { text: 'Core Concepts', @@ -30,8 +31,8 @@ export default defineConfig({ { text: 'Handlers & Extractors', link: '/guide/handlers' }, { text: 'Middleware', link: '/guide/middleware' }, { text: 'Streaming', link: '/guide/streaming' }, - { text: 'Proxying', link: '/guide/proxying' } - ] + { text: 'Proxying', link: '/guide/proxying' }, + ], }, { text: 'Adapters', @@ -39,25 +40,28 @@ export default defineConfig({ { text: 'Overview', link: '/guide/adapters/overview' }, { text: 'Fastly Compute', link: '/guide/adapters/fastly' }, { text: 'Cloudflare Workers', link: '/guide/adapters/cloudflare' }, - { text: 'Axum (Native)', link: '/guide/adapters/axum' } - ] + { text: 'Axum (Native)', link: '/guide/adapters/axum' }, + ], }, { text: 'Reference', items: [ - { text: 'Configuration (edgezero.toml)', link: '/guide/configuration' }, - { text: 'CLI Reference', link: '/guide/cli-reference' } - ] - } + { + text: 'Configuration (edgezero.toml)', + link: '/guide/configuration', + }, + { text: 'CLI Reference', link: '/guide/cli-reference' }, + ], + }, ], socialLinks: [ - { icon: 'github', link: 'https://github.com/stackpop/edgezero' } + { icon: 'github', link: 'https://github.com/stackpop/edgezero' }, ], footer: { message: 'Released under the MIT License.', - copyright: 'Copyright 2024-present Stackpop' - } - } + copyright: 'Copyright 2025-present Stackpop', + }, + }, }) diff --git a/docs/eslint.config.js b/docs/eslint.config.js new file mode 100644 index 0000000..50481a7 --- /dev/null +++ b/docs/eslint.config.js @@ -0,0 +1,20 @@ +import js from '@eslint/js' +import tseslint from 'typescript-eslint' + +export default [ + { + ignores: ['.vitepress/cache/**', '.vitepress/dist/**', 'node_modules/**'], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.ts', '**/*.mts'], + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['.vitepress/*.mts'], + }, + }, + }, + }, +] diff --git a/docs/guide/adapters/axum.md b/docs/guide/adapters/axum.md index b1ac617..82d1b65 100644 --- a/docs/guide/adapters/axum.md +++ b/docs/guide/adapters/axum.md @@ -12,7 +12,7 @@ The Axum adapter provides: ## Project Setup -When scaffolding with `edgezero new my-app --adapters axum`, you get: +When scaffolding with `edgezero new my-app`, the Axum adapter includes: ``` crates/my-app-adapter-axum/ @@ -28,20 +28,16 @@ The Axum entrypoint wires the adapter: ```rust use edgezero_adapter_axum::AxumDevServer; +use edgezero_core::app::Hooks; use my_app_core::App; -#[tokio::main] -async fn main() { - // Initialize standard logging - env_logger::init(); - - let app = App::build(); - - AxumDevServer::new(app) - .bind("127.0.0.1:8787") - .run() - .await - .unwrap(); +fn main() { + let app = App::build_app(); + let router = app.router().clone(); + if let Err(err) = AxumDevServer::new(router).run() { + eprintln!("axum adapter failed: {err}"); + std::process::exit(1); + } } ``` @@ -53,11 +49,7 @@ The `edgezero dev` command uses the Axum adapter: edgezero dev ``` -This starts a server at `http://127.0.0.1:8787` with: - -- Hot reload support (via cargo watch integration) -- Standard logging to stdout -- Full handler debugging +This starts a server at `http://127.0.0.1:8787` with standard logging to stdout. ### Manual Start @@ -91,8 +83,9 @@ The Axum adapter provides a native HTTP client for proxying: ```rust use edgezero_adapter_axum::AxumProxyClient; +use edgezero_core::proxy::ProxyService; -let client = AxumProxyClient::new(); +let client = AxumProxyClient::default(); let response = ProxyService::new(client).forward(request).await?; ``` @@ -100,29 +93,13 @@ This uses `reqwest` under the hood for outbound HTTP requests. ## Logging -Use any standard Rust logging implementation: - -```rust -use log::{info, error}; - -#[tokio::main] -async fn main() { - // Simple logger - env_logger::init(); - - // Or use tracing - // tracing_subscriber::fmt::init(); - - info!("Starting server..."); -} -``` - -Configure log levels via environment variable: +The Axum adapter's `run_app` helper installs `simple_logger` and reads logging configuration +from `edgezero.toml` (level and `echo_stdout`). If you want a different logger, wire your own +entrypoint using `App::build_app()` and `AxumDevServer`. -```bash -RUST_LOG=info edgezero dev -RUST_LOG=my_app=debug,edgezero_core=info edgezero dev -``` +::: tip Logging status +`run_app` wires logging automatically; custom entrypoints should install a logger explicitly. +::: ## Testing @@ -132,19 +109,20 @@ The Axum adapter enables standard Rust testing: #[cfg(test)] mod tests { use super::*; + use edgezero_core::app::Hooks; use edgezero_core::http::{Request, Method}; - + #[tokio::test] async fn test_handler() { - let app = App::build(); + let app = App::build_app(); let router = app.router(); - + let request = Request::builder() .method(Method::GET) .uri("/hello") .body(Body::empty()) .unwrap(); - + let response = router.oneshot(request).await.unwrap(); assert_eq!(response.status(), 200); } @@ -176,28 +154,11 @@ CMD ["my-app-adapter-axum"] ## Configuration -Configure the dev server in `axum.toml`: +Configure the Axum adapter in `edgezero.toml`. See [Configuration](/guide/configuration) for the full +manifest reference. -```toml -[server] -host = "127.0.0.1" -port = 8787 - -[logging] -level = "info" -``` - -Or via `edgezero.toml`: - -```toml -[adapters.axum.adapter] -crate = "crates/my-app-adapter-axum" -manifest = "crates/my-app-adapter-axum/axum.toml" - -[adapters.axum.commands] -build = "cargo build --release -p my-app-adapter-axum" -serve = "cargo run -p my-app-adapter-axum" -``` +The `axum.toml` file is used by the Axum CLI helper to locate the crate and display the port. +The runtime currently binds to `127.0.0.1:8787` regardless of the `axum.toml` port value. ## Development Workflow @@ -212,14 +173,14 @@ A typical development workflow: ## Differences from Edge Adapters -| Aspect | Axum | Fastly/Cloudflare | -|--------|------|-------------------| -| Compilation | Native | Wasm | -| Cold start | ~0ms | ~0ms (Wasm) | -| Memory | Unlimited | 128MB typical | -| Filesystem | Full access | Sandboxed | -| Network | Direct | Backend/fetch | -| Concurrency | Multi-threaded | Single-threaded | +| Aspect | Axum | Fastly/Cloudflare | +| ----------- | -------------- | ----------------- | +| Compilation | Native | Wasm | +| Cold start | ~0ms | ~0ms (Wasm) | +| Memory | Unlimited | 128MB typical | +| Filesystem | Full access | Sandboxed | +| Network | Direct | Backend/fetch | +| Concurrency | Multi-threaded | Single-threaded | ::: tip Development Parity While Axum provides a convenient development environment, always test on actual edge platforms before deploying. Some edge-specific features (KV stores, geolocation) aren't available in the Axum adapter. diff --git a/docs/guide/adapters/cloudflare.md b/docs/guide/adapters/cloudflare.md index f757d23..00e165a 100644 --- a/docs/guide/adapters/cloudflare.md +++ b/docs/guide/adapters/cloudflare.md @@ -9,7 +9,7 @@ Deploy EdgeZero applications to Cloudflare Workers using WebAssembly. ## Project Setup -When scaffolding with `edgezero new my-app --adapters cloudflare`, you get: +When scaffolding with `edgezero new my-app`, the Cloudflare adapter includes: ``` crates/my-app-adapter-cloudflare/ @@ -37,15 +37,15 @@ command = "cargo build --release --target wasm32-unknown-unknown" The Cloudflare entrypoint wires the adapter: ```rust -use edgezero_adapter_cloudflare::{dispatch, init_logger}; +use edgezero_adapter_cloudflare::dispatch; +use edgezero_core::app::Hooks; use my_app_core::App; use worker::*; #[event(fetch)] -async fn main(req: Request, env: Env, _ctx: Context) -> Result { - init_logger(); - let app = App::build(); - dispatch(&app, req).await +async fn main(req: Request, env: Env, ctx: Context) -> Result { + let app = App::build_app(); + dispatch(&app, req, env, ctx).await } ``` @@ -93,8 +93,9 @@ Cloudflare Workers use the global `fetch` API for outbound requests: ```rust use edgezero_adapter_cloudflare::CloudflareProxyClient; +use edgezero_core::proxy::ProxyService; -let client = CloudflareProxyClient::new(); +let client = CloudflareProxyClient; let response = ProxyService::new(client).forward(request).await?; ``` @@ -102,40 +103,31 @@ Unlike Fastly, there's no backend configuration needed - Workers can fetch any U ## Logging -Cloudflare Workers log via `console.log`. Initialize the logger: +EdgeZero does not install a Cloudflare logger by default. Use your preferred logger (for example +`console_log` or your own `log` implementation), and view output in Wrangler or the Cloudflare +dashboard. -```rust -use edgezero_adapter_cloudflare::init_logger; - -fn main() { - init_logger(); -} -``` - -Configure logging level in `edgezero.toml`: - -```toml -[adapters.cloudflare.logging] -level = "info" -``` - -View logs in the Wrangler output or Cloudflare dashboard. +::: tip Logging status +Cloudflare logging is opt-in; install a logger (such as `console_log`) in your entrypoint if you +need structured output. +::: ## Context Access -Access Cloudflare-specific APIs via the request context: +Access Cloudflare-specific APIs via the request context extensions: ```rust +use edgezero_core::context::RequestContext; use edgezero_adapter_cloudflare::CloudflareRequestContext; -#[action] -async fn handler(RequestContext(ctx): RequestContext) -> Response { - if let Some(cf_ctx) = ctx.extensions().get::() { +async fn handler(ctx: RequestContext) -> Result { + if let Some(cf_ctx) = CloudflareRequestContext::get(ctx.request()) { // Access Cloudflare-specific data - let cf = cf_ctx.cf(); + let env = cf_ctx.env(); + let ctx = cf_ctx.ctx(); // ... } - + // ... } ``` @@ -181,24 +173,9 @@ bindings = [ ## Streaming -Cloudflare Workers support streaming via `ReadableStream`: +Cloudflare Workers support streaming via `ReadableStream`. The adapter automatically converts `Body::stream` to Cloudflare's streaming format. -```rust -#[action] -async fn stream() -> Response { - let stream = async_stream::stream! { - for i in 0..100 { - yield Ok::<_, std::io::Error>(format!("chunk {}\n", i).into_bytes()); - } - }; - - Response::builder() - .body(Body::stream(stream)) - .unwrap() -} -``` - -The adapter converts EdgeZero streams to Cloudflare's `ReadableStream` format. +See the [Streaming guide](/guide/streaming) for examples and patterns. ## Testing @@ -212,35 +189,17 @@ Note: Some tests require `wasm-bindgen-test-runner` for execution. ## Manifest Configuration -Full `edgezero.toml` Cloudflare configuration: - -```toml -[adapters.cloudflare.adapter] -crate = "crates/my-app-adapter-cloudflare" -manifest = "crates/my-app-adapter-cloudflare/wrangler.toml" - -[adapters.cloudflare.build] -target = "wasm32-unknown-unknown" -profile = "release" - -[adapters.cloudflare.commands] -build = "cargo build --release --target wasm32-unknown-unknown -p my-app-adapter-cloudflare" -serve = "wrangler dev --config crates/my-app-adapter-cloudflare/wrangler.toml" -deploy = "wrangler publish --config crates/my-app-adapter-cloudflare/wrangler.toml" - -[adapters.cloudflare.logging] -level = "info" -``` +Configure the Cloudflare adapter in `edgezero.toml`. See [Configuration](/guide/configuration) for the full manifest reference. ## Comparison with Fastly -| Feature | Cloudflare Workers | Fastly Compute | -|---------|-------------------|----------------| -| Target | `wasm32-unknown-unknown` | `wasm32-wasip1` | -| Outbound requests | Global `fetch` | Named backends | -| Storage | KV, Durable Objects, R2 | KV Store, Object Store | -| Logging | `console.log` | Log endpoints | -| CLI | Wrangler | Fastly CLI | +| Feature | Cloudflare Workers | Fastly Compute | +| ----------------- | ------------------------ | ---------------------- | +| Target | `wasm32-unknown-unknown` | `wasm32-wasip1` | +| Outbound requests | Global `fetch` | Named backends | +| Storage | KV, Durable Objects, R2 | KV Store, Object Store | +| Logging | `console.log` | Log endpoints | +| CLI | Wrangler | Fastly CLI | ## Next Steps diff --git a/docs/guide/adapters/fastly.md b/docs/guide/adapters/fastly.md index 13b95d4..ead2d83 100644 --- a/docs/guide/adapters/fastly.md +++ b/docs/guide/adapters/fastly.md @@ -10,7 +10,7 @@ Deploy EdgeZero applications to Fastly's Compute@Edge platform using WebAssembly ## Project Setup -When scaffolding with `edgezero new my-app --adapters fastly`, you get: +When scaffolding with `edgezero new my-app`, the Fastly adapter includes: ``` crates/my-app-adapter-fastly/ @@ -41,14 +41,14 @@ authors = ["you@example.com"] The Fastly entrypoint wires the adapter: ```rust -use edgezero_adapter_fastly::{dispatch, init_logger}; +use edgezero_adapter_fastly::dispatch; +use edgezero_core::app::Hooks; use my_app_core::App; #[fastly::main] -async fn main(req: fastly::Request) -> Result { - init_logger(); - let app = App::build(); - dispatch(&app, req).await +fn main(req: fastly::Request) -> Result { + let app = App::build_app(); + dispatch(&app, req) } ``` @@ -94,89 +94,67 @@ fastly compute deploy ## Backends -Fastly routes outbound requests through named backends. Configure them in `fastly.toml`: - -```toml -[local_server.backends] - [local_server.backends."api"] - url = "https://api.example.com" - - [local_server.backends."cdn"] - url = "https://cdn.example.com" -``` - -Use backends in your proxy code: +EdgeZero's Fastly proxy client uses **dynamic backends** derived from the target URI (host + scheme). +You do not need to predeclare backends in `fastly.toml` for EdgeZero proxying. ```rust use edgezero_adapter_fastly::FastlyProxyClient; +use edgezero_core::proxy::ProxyService; -let client = FastlyProxyClient::new("api"); +let client = FastlyProxyClient; let response = ProxyService::new(client).forward(request).await?; ``` ## Logging -Fastly uses endpoint-based logging. Initialize the logger in your entrypoint: +Fastly uses endpoint-based logging. Configure logging in `edgezero.toml`: + +```toml +[adapters.fastly.logging] +endpoint = "stdout" +level = "info" +echo_stdout = true +``` + +To initialize logging manually, call `init_logger` with explicit settings: ```rust use edgezero_adapter_fastly::init_logger; +use log::LevelFilter; fn main() { - init_logger(); // Uses stdout by default - // or with custom endpoint: - // init_logger_with_endpoint("my-logging-endpoint"); + init_logger("stdout", LevelFilter::Info, true).expect("init logger"); } ``` -Configure logging in `edgezero.toml`: - -```toml -[adapters.fastly.logging] -endpoint = "stdout" -level = "info" -echo_stdout = true -``` +::: tip Logging status +Fastly logging is wired when you call `init_logger` (or `run_app`); otherwise no logger is installed. +::: ## Context Access -Access Fastly-specific APIs via the request context: +Access Fastly-specific APIs via the request context extensions: ```rust +use edgezero_core::context::RequestContext; use edgezero_adapter_fastly::FastlyRequestContext; -#[action] -async fn handler(RequestContext(ctx): RequestContext) -> Response { +async fn handler(ctx: RequestContext) -> Result { // Access Fastly context from extensions - if let Some(fastly_ctx) = ctx.extensions().get::() { - let client_ip = fastly_ctx.client_ip(); - let geo = fastly_ctx.geo(); + if let Some(fastly_ctx) = FastlyRequestContext::get(ctx.request()) { + let client_ip = fastly_ctx.client_ip; // ... } - + // ... } ``` ## Streaming -Fastly supports native streaming via `stream_to_client`: +Fastly supports native streaming via `stream_to_client`. The adapter automatically converts `Body::stream` to Fastly's streaming APIs. -```rust -#[action] -async fn stream() -> Response { - let stream = async_stream::stream! { - for i in 0..100 { - yield Ok::<_, std::io::Error>(format!("chunk {}\n", i).into_bytes()); - } - }; - - Response::builder() - .body(Body::stream(stream)) - .unwrap() -} -``` - -The adapter automatically uses Fastly's streaming APIs for optimal performance. +See the [Streaming guide](/guide/streaming) for examples and patterns. ## Testing @@ -196,27 +174,7 @@ If Viceroy reports keychain access errors on macOS, use Wasmtime as the test run ## Manifest Configuration -Full `edgezero.toml` Fastly configuration: - -```toml -[adapters.fastly.adapter] -crate = "crates/my-app-adapter-fastly" -manifest = "crates/my-app-adapter-fastly/fastly.toml" - -[adapters.fastly.build] -target = "wasm32-wasip1" -profile = "release" - -[adapters.fastly.commands] -build = "cargo build --release --target wasm32-wasip1 -p my-app-adapter-fastly" -serve = "fastly compute serve -C crates/my-app-adapter-fastly" -deploy = "fastly compute deploy -C crates/my-app-adapter-fastly" - -[adapters.fastly.logging] -endpoint = "stdout" -level = "info" -echo_stdout = true -``` +Configure the Fastly adapter in `edgezero.toml`. See [Configuration](/guide/configuration) for the full manifest reference. ## Next Steps diff --git a/docs/guide/adapters/overview.md b/docs/guide/adapters/overview.md index bd04ca3..e705f9d 100644 --- a/docs/guide/adapters/overview.md +++ b/docs/guide/adapters/overview.md @@ -7,7 +7,7 @@ Adapters bridge provider-specific HTTP primitives into EdgeZero's portable model Adapters translate provider-specific HTTP primitives into the portable `App` in `edgezero-core`. They must: - Preserve request semantics -- Stream responses without buffering +- Stream responses without buffering where the provider supports it - Expose provider context - Offer a proxy bridge so handlers can forward traffic without knowing which platform they are on @@ -18,7 +18,7 @@ Each adapter exposes an `into_core_request` helper that accepts the provider's r - **Preserve the HTTP method** exactly (`GET`, `POST`, etc.) - **Parse the full URI** (path and query string) into an `http::Uri`. Reject invalid URIs with `EdgeError::bad_request` - **Copy all headers** into the core request. Provider-specific headers may be filtered only when they clash with platform defaults -- **Consume the request body** into an `edgezero_core::body::Body`. If the provider offers a streaming API, it should be exposed via `Body::Stream`; otherwise a single buffered chunk is acceptable +- **Consume the request body** into an `edgezero_core::body::Body`. Adapters may buffer inbound bodies today; streaming input should be preserved where available - **Insert a provider context struct** (e.g., `FastlyRequestContext`) into the request extensions. The context should expose metadata such as client IP addresses or environment handles so handlers can reach platform APIs ## Response Conversion @@ -37,7 +37,7 @@ Adapters surface a `dispatch` function that bridges from the provider event loop 1. Convert the incoming provider request with `into_core_request` 2. Await the router future 3. Convert the resulting `Response` back into the provider type -4. Map any `EdgeError` into the provider's error type so failures surface as HTTP 5xx responses instead of panicking +4. Map any `EdgeError` into the provider's error type so failures surface as provider errors (often 5xx) instead of panicking This helper is what demo entrypoints and adapters call when wiring their platform-specific main functions. @@ -48,12 +48,14 @@ Adapters implement `edgezero_core::proxy::ProxyClient` so handlers can forward o - Accept a `ProxyRequest` created with `ProxyRequest::from_request` - Build and send an outbound provider request, reusing headers and streaming the body without buffering - Convert the provider response into a `ProxyResponse`, again preserving streaming behaviour and normalising encodings -- Attach a diagnostic header (e.g., `x-edgezero-proxy`) identifying which adapter forwarded the call +- Attach a diagnostic header (e.g., `x-edgezero-proxy`) identifying which adapter forwarded the call (Fastly and Cloudflare do this today) - Surface provider errors as `EdgeError::internal` so applications can decide how to respond ## Logging Initialisation -Each adapter exports an `init_logger` helper for platform-specific logging backends (`log_fastly` or `console_log!`). Applications should call it before building the router. New adapters should provide a comparable helper so apps consistently opt into logging. +Each adapter exports an `init_logger` helper for platform-specific logging backends. Fastly wires +`log_fastly`, Cloudflare currently no-ops, and Axum uses `simple_logger` in its `run_app` helper. +New adapters should provide a comparable helper so apps consistently opt into logging. ## Contract Tests @@ -98,8 +100,8 @@ Adapters that fulfil these steps can be dropped into the EdgeZero CLI without re ## Available Adapters -| Adapter | Platform | Target | Status | -|---------|----------|--------|--------| -| [Fastly](/guide/adapters/fastly) | Fastly Compute@Edge | `wasm32-wasip1` | Stable | -| [Cloudflare](/guide/adapters/cloudflare) | Cloudflare Workers | `wasm32-unknown-unknown` | Stable | -| [Axum](/guide/adapters/axum) | Native (Tokio) | Host | Stable | +| Adapter | Platform | Target | Status | +| ---------------------------------------- | ------------------- | ------------------------ | ------ | +| [Fastly](/guide/adapters/fastly) | Fastly Compute@Edge | `wasm32-wasip1` | Stable | +| [Cloudflare](/guide/adapters/cloudflare) | Cloudflare Workers | `wasm32-unknown-unknown` | Stable | +| [Axum](/guide/adapters/axum) | Native (Tokio) | Host | Stable | diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md index 940c6b3..2ca1411 100644 --- a/docs/guide/architecture.md +++ b/docs/guide/architecture.md @@ -25,7 +25,7 @@ edgezero/ - **Routing** - `RouterService` with path parameter matching via `matchit` - **Request/Response** - Portable `http::Request` and `http::Response` types - **Body** - Unified body type supporting buffered and streaming modes -- **Extractors** - `Json`, `Path`, `Query`, `ValidatedQuery` +- **Extractors** - `Json`, `Path`, `Query`, `Form`, `Headers`, and `Validated*` variants - **Middleware** - Composable middleware chain with async support - **Manifest** - `edgezero.toml` parsing and validation - **Compression** - Shared gzip/brotli stream decoders @@ -117,16 +117,14 @@ Adapters translate between provider-specific types and the portable core model: ## Feature Flags -EdgeZero uses feature flags for optional functionality: +Adapter crates use feature flags to gate provider SDKs and CLI integration: -| Feature | Crate | Purpose | -|---------|-------|---------| -| `json` | edgezero-core | JSON extractor/responder support | -| `form` | edgezero-core | Form data extraction | -| `validator` | edgezero-core | Validated extractors | -| `fastly` | edgezero-adapter-fastly | Fastly SDK integration | -| `cloudflare` | edgezero-adapter-cloudflare | Workers SDK integration | -| `dev-example` | edgezero-cli | Bundled demo app for development | +| Feature | Crate | Purpose | +| ------------- | --------------------------- | -------------------------------------- | +| `fastly` | edgezero-adapter-fastly | Fastly SDK integration | +| `cloudflare` | edgezero-adapter-cloudflare | Workers SDK integration | +| `cli` | adapter crates | Register adapters and scaffolding data | +| `dev-example` | edgezero-cli | Bundled demo app for development | ## Next Steps diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index dfbcca0..6247ddb 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -27,22 +27,22 @@ edgezero new [options] ``` **Arguments:** + - `` - Project name (used for directory and crate names) **Options:** -- `--adapters ` - Comma-separated adapters to include (default: `fastly,cloudflare,axum`) + +- `--dir ` - Directory to create the project in (default: current directory) +- `--local-core` - Use local path dependency for edgezero-core (development only) **Examples:** ```bash -# Create project with all adapters +# Create project with all registered adapters edgezero new my-app -# Create project with specific adapters -edgezero new my-app --adapters fastly,axum - -# Create project with only Cloudflare -edgezero new my-app --adapters cloudflare +# Create in a specific directory +edgezero new my-app --dir /path/to/projects ``` **Generated structure:** @@ -53,37 +53,30 @@ my-app/ ├── edgezero.toml ├── crates/ │ ├── my-app-core/ -│ ├── my-app-adapter-fastly/ (if --adapters includes fastly) -│ ├── my-app-adapter-cloudflare/ (if --adapters includes cloudflare) -│ └── my-app-adapter-axum/ (if --adapters includes axum) +│ ├── my-app-adapter-fastly/ +│ ├── my-app-adapter-cloudflare/ +│ └── my-app-adapter-axum/ ``` +The scaffolder includes all adapters registered at CLI build time. + ### edgezero dev Start the local development server: ```bash -edgezero dev [options] +edgezero dev ``` -**Options:** -- `--port ` - Port to listen on (default: 8787) -- `--host ` - Host to bind to (default: 127.0.0.1) - -**Examples:** +**Example:** ```bash -# Start dev server with defaults edgezero dev - -# Start on custom port -edgezero dev --port 3000 - -# Bind to all interfaces -edgezero dev --host 0.0.0.0 +# Server starts at http://127.0.0.1:8787 ``` -The dev server uses the Axum adapter and reads configuration from `edgezero.toml`. +If `edgezero.toml` defines an Axum adapter command, `edgezero dev` delegates to it. Otherwise it +starts the built-in dev server (default routes). ### edgezero build @@ -94,6 +87,7 @@ edgezero build --adapter ``` **Arguments:** + - `--adapter ` - Target adapter (`fastly`, `cloudflare`, `axum`) **Examples:** @@ -120,6 +114,7 @@ edgezero serve --adapter ``` **Arguments:** + - `--adapter ` - Target adapter (`fastly`, `cloudflare`, `axum`) **Examples:** @@ -136,6 +131,7 @@ edgezero serve --adapter axum ``` **Provider behavior:** + - **Fastly**: Runs `fastly compute serve` - **Cloudflare**: Runs `wrangler dev` - **Axum**: Runs `cargo run -p ` @@ -149,6 +145,7 @@ edgezero deploy --adapter ``` **Arguments:** + - `--adapter ` - Target adapter (`fastly`, `cloudflare`) **Examples:** @@ -162,6 +159,7 @@ edgezero deploy --adapter cloudflare ``` **Provider behavior:** + - **Fastly**: Runs `fastly compute deploy` - **Cloudflare**: Runs `wrangler publish` @@ -173,35 +171,23 @@ The `axum` adapter doesn't support `deploy` - use standard container/binary depl The CLI respects these environment variables: -| Variable | Description | -|----------|-------------| -| `RUST_LOG` | Log level for dev server | +| Variable | Description | +| ------------------- | ------------------------------------------- | | `EDGEZERO_MANIFEST` | Path to manifest (default: `edgezero.toml`) | -## Exit Codes - -| Code | Meaning | -|------|---------| -| 0 | Success | -| 1 | General error | -| 2 | Missing configuration | -| 3 | Build failure | -| 4 | Missing adapter | - ## Working Directory -All commands expect to run from the project root where `edgezero.toml` is located. The CLI searches up the directory tree for the manifest if not found in the current directory. +All commands expect to run from the project root where `edgezero.toml` is located. If the file is +missing, the CLI falls back to built-in adapters (when compiled in) instead of manifest-driven +commands. ## Adapter Discovery -Adapters register themselves via the `edgezero-adapter` registry. The CLI discovers available adapters at runtime: +Adapters register themselves via the `edgezero-adapter` registry at build time. There is currently +no `edgezero --list-adapters` command; the scaffolder includes all adapters that were compiled in. -```bash -# List available adapters -edgezero --list-adapters -``` +Built-in adapters (default CLI build): -Built-in adapters: - `fastly` - Fastly Compute@Edge - `cloudflare` - Cloudflare Workers - `axum` - Native Axum/Tokio @@ -215,6 +201,7 @@ error: target may not be installed ``` Install the required target: + ```bash rustup target add wasm32-wasip1 # For Fastly rustup target add wasm32-unknown-unknown # For Cloudflare @@ -222,14 +209,9 @@ rustup target add wasm32-unknown-unknown # For Cloudflare ### Manifest Not Found -``` -error: edgezero.toml not found -``` - -Ensure you're in the project root or set `EDGEZERO_MANIFEST`: -```bash -EDGEZERO_MANIFEST=/path/to/edgezero.toml edgezero dev -``` +If you rely on manifest-driven commands, ensure `edgezero.toml` exists or set `EDGEZERO_MANIFEST`. +When no manifest is present, the CLI falls back to built-in adapter implementations (if compiled +in) instead of using manifest commands. ### Provider CLI Not Found @@ -238,6 +220,7 @@ error: fastly: command not found ``` Install the provider CLI: + - Fastly: https://developer.fastly.com/learning/compute/ - Cloudflare: `npm install -g wrangler` diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 8fb099f..6d58288 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -9,8 +9,6 @@ New workspaces scaffolded with `edgezero new` include this manifest by default. ```toml [app] name = "my-app" -version = "0.1.0" -kind = "http" entry = "crates/my-app-core" middleware = ["edgezero_core::middleware::RequestLogger"] @@ -34,19 +32,15 @@ The `[app]` section defines application metadata: ```toml [app] name = "demo" -version = "0.1.0" -kind = "http" entry = "crates/demo-core" middleware = ["edgezero_core::middleware::RequestLogger"] ``` -| Field | Required | Description | -|-------|----------|-------------| -| `name` | Yes | Display name for the application | -| `version` | No | Semantic version | -| `kind` | No | Application type (currently only `http`) | -| `entry` | Yes | Path to the core crate containing handlers | -| `middleware` | No | List of middleware to apply globally | +| Field | Required | Description | +| ------------ | -------- | -------------------------------------------------------------------- | +| `name` | No | Display name for the application (defaults to "EdgeZero App") | +| `entry` | No | Path to the core crate containing handlers (recommended for tooling) | +| `middleware` | No | List of middleware to apply globally | ### Middleware @@ -61,6 +55,7 @@ middleware = [ ``` Each item must be: + - A publicly accessible path - Either a unit struct or zero-argument constructor - Implementing `edgezero_core::middleware::Middleware` @@ -85,14 +80,15 @@ adapters = ["fastly", "cloudflare"] body-mode = "buffered" ``` -| Field | Required | Description | -|-------|----------|-------------| -| `id` | No | Stable identifier for tooling | -| `path` | Yes | URI template (`{param}` for params, `{*rest}` for catch-all) | -| `methods` | No | Allowed HTTP methods (defaults to `GET`) | -| `handler` | Yes | Path to handler function | -| `adapters` | No | Which adapters expose this route (empty = all) | -| `body-mode` | No | `buffered` or `stream` | +| Field | Required | Description | +| ------------- | -------- | ------------------------------------------------------------ | +| `id` | No | Stable identifier for tooling | +| `path` | Yes | URI template (`{param}` for params, `{*rest}` for catch-all) | +| `methods` | No | Allowed HTTP methods (defaults to `GET`) | +| `handler` | No | Path to handler function (required for `app!` route wiring) | +| `adapters` | No | Which adapters expose this route (empty = all) | +| `description` | No | Human-readable description for docs or tooling | +| `body-mode` | No | `buffered` or `stream` | ## Environment Section @@ -114,22 +110,24 @@ env = "API_TOKEN" ### Variables -| Field | Required | Description | -|-------|----------|-------------| -| `name` | Yes | Variable name in application | -| `env` | No | Environment key (defaults to `name`) | -| `value` | No | Default value | -| `adapters` | No | Limit to specific adapters | +| Field | Required | Description | +| ------------- | -------- | ------------------------------------ | +| `name` | Yes | Variable name in application | +| `description` | No | Human-readable description | +| `env` | No | Environment key (defaults to `name`) | +| `value` | No | Default value | +| `adapters` | No | Limit to specific adapters | Variables with a default `value` are injected when running CLI commands. ### Secrets -| Field | Required | Description | -|-------|----------|-------------| -| `name` | Yes | Secret name in application | -| `env` | No | Environment key (defaults to `name`) | -| `adapters` | No | Limit to specific adapters | +| Field | Required | Description | +| ------------- | -------- | ------------------------------------ | +| `name` | Yes | Secret name in application | +| `description` | No | Human-readable description | +| `env` | No | Environment key (defaults to `name`) | +| `adapters` | No | Limit to specific adapters | Secrets must be present in the environment; missing secrets abort CLI commands with an error. @@ -159,44 +157,47 @@ echo_stdout = true ### Adapter Metadata -| Field | Description | -|-------|-------------| -| `crate` | Path to adapter crate | +| Field | Description | +| ---------- | ------------------------------------------------------ | +| `crate` | Path to adapter crate | | `manifest` | Path to provider manifest (fastly.toml, wrangler.toml) | ### Build Configuration -| Field | Description | -|-------|-------------| -| `target` | Rust compilation target | -| `profile` | Build profile (`release`, `dev`) | -| `features` | Cargo features to enable | +| Field | Description | +| ---------- | -------------------------------- | +| `target` | Rust compilation target | +| `profile` | Build profile (`release`, `dev`) | +| `features` | Cargo features to enable | ### Commands -| Field | Description | -|-------|-------------| -| `build` | Command for `edgezero build --adapter ` | -| `serve` | Command for `edgezero serve --adapter ` | +| Field | Description | +| -------- | ---------------------------------------------- | +| `build` | Command for `edgezero build --adapter ` | +| `serve` | Command for `edgezero serve --adapter ` | | `deploy` | Command for `edgezero deploy --adapter ` | When commands are omitted, the CLI falls back to built-in adapter helpers. ### Logging -| Field | Adapters | Description | -|-------|----------|-------------| -| `endpoint` | Fastly | Log endpoint name | -| `level` | All | Log level: `trace`, `debug`, `info`, `warn`, `error`, `off` | -| `echo_stdout` | Fastly | Mirror logs to stdout | +Logging can be configured per adapter under `[adapters..logging]` or via a top-level +`[logging.]` block. If both are present, the adapter-specific block takes precedence. + +| Field | Adapters | Description | +| ------------- | ------------ | ----------------------------------------------------------- | +| `endpoint` | Fastly | Log endpoint name | +| `level` | All | Log level: `trace`, `debug`, `info`, `warn`, `error`, `off` | +| `echo_stdout` | Fastly, Axum | Mirror logs to stdout | + +Note: Cloudflare logging is not wired to a built-in logger yet. ## Full Example ```toml [app] name = "my-app" -version = "0.1.0" -kind = "http" entry = "crates/my-app-core" middleware = [ "edgezero_core::middleware::RequestLogger", @@ -288,10 +289,11 @@ edgezero_core::app!("../../edgezero.toml"); ``` The macro: + - Parses HTTP triggers - Generates route registration - Wires middleware from the manifest -- Creates the `App` struct with `build()` method +- Creates the `App` struct that implements `Hooks` (use `App::build_app()`) ### ManifestLoader @@ -299,17 +301,25 @@ Load the manifest programmatically: ```rust use edgezero_core::manifest::ManifestLoader; - -let manifest = ManifestLoader::load("edgezero.toml")?; -println!("App name: {}", manifest.app.name); +use std::path::Path; + +let manifest = ManifestLoader::from_path(Path::new("edgezero.toml"))?; +let app_name = manifest + .manifest() + .app + .name + .as_deref() + .unwrap_or("EdgeZero App"); +println!("App name: {}", app_name); ``` ## Validation `ManifestLoader` validates: -- Non-empty trigger paths and handlers -- Well-formed logging levels -- Required fields present + +- Non-empty string fields when present (names, paths, commands) +- Supported HTTP methods and `body-mode` values +- Well-formed logging levels and adapter logging config Errors are surfaced at startup or during macro expansion. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 6b13468..9befc2a 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -4,7 +4,7 @@ This guide walks you through creating your first EdgeZero application. ## Prerequisites -- Rust toolchain (1.70+) +- Rust toolchain (stable; see `.tool-versions` in the repo) - For Fastly: `wasm32-wasip1` target and the Fastly CLI - For Cloudflare: `wasm32-unknown-unknown` target and Wrangler @@ -18,15 +18,15 @@ cargo install --path crates/edgezero-cli ## Create a New Project -Scaffold a new EdgeZero app targeting your preferred adapters: +Scaffold a new EdgeZero app: ```bash -# Create an app with Fastly, Cloudflare, and Axum adapters -edgezero new my-app --adapters fastly cloudflare axum +edgezero new my-app cd my-app ``` This generates a workspace with: + - `crates/my-app-core` - Your shared handlers and routing logic - `crates/my-app-adapter-fastly` - Fastly Compute entrypoint - `crates/my-app-adapter-cloudflare` - Cloudflare Workers entrypoint diff --git a/docs/guide/handlers.md b/docs/guide/handlers.md index 6e6442f..0d735b7 100644 --- a/docs/guide/handlers.md +++ b/docs/guide/handlers.md @@ -24,6 +24,7 @@ async fn create_user(Json(body): Json) -> Text { ``` The macro: + - Generates the `FromRequest` boilerplate for each extractor - Handles async execution - Converts the return type into a proper response @@ -120,26 +121,77 @@ async fn create_post(ValidatedJson(body): ValidatedJson) -> Text Text { + let token = headers + .get("authorization") + .and_then(|v| v.to_str().ok()) + .unwrap_or("none"); + Text::new(format!("Auth: {}", token)) +} +``` + +### Form Data + +Parse URL-encoded form bodies: + +```rust +use edgezero_core::extractor::Form; + +#[derive(serde::Deserialize)] +struct ContactForm { + name: String, + email: String, +} + +#[action] +async fn submit_form(Form(data): Form) -> Text { + Text::new(format!("Received from: {}", data.email)) +} +``` + +Use `ValidatedForm` for form data with validation, and `ValidatedPath` for validated path parameters. + ### Request Context -Access the full request context for headers, method, URI, etc: +For full request access, handlers can receive `RequestContext` directly (no `#[action]` needed): ```rust use edgezero_core::context::RequestContext; +use edgezero_core::error::EdgeError; -#[action] -async fn inspect(RequestContext(ctx): RequestContext) -> Text { - let method = ctx.method(); - let path = ctx.uri().path(); - let user_agent = ctx.headers() +async fn inspect(ctx: RequestContext) -> Result, EdgeError> { + let method = ctx.request().method(); + let path = ctx.request().uri().path(); + let user_agent = ctx.request().headers() .get("user-agent") .and_then(|v| v.to_str().ok()) .unwrap_or("unknown"); - - Text::new(format!("{} {} from {}", method, path, user_agent)) + + Ok(Text::new(format!("{} {} from {}", method, path, user_agent))) } ``` +`RequestContext` provides these methods: + +| Method | Returns | +| ---------------- | ------------------------------------------ | +| `request()` | `&Request` - full HTTP request | +| `path_params()` | `&PathParams` - raw path parameters | +| `path::()` | Deserialize path params to `T` | +| `query::()` | Deserialize query string to `T` | +| `json::()` | Deserialize JSON body to `T` | +| `form::()` | Deserialize form body to `T` | +| `body()` | `&Body` - raw request body | +| `proxy_handle()` | `Option` - adapter proxy hook | + ## Response Types ### Text Responses @@ -155,8 +207,12 @@ async fn hello() -> Text<&'static str> { ### JSON Responses +Build JSON responses using `Body::json`: + ```rust -use edgezero_core::response::Json; +use edgezero_core::body::Body; +use edgezero_core::http::{Response, StatusCode}; +use edgezero_core::error::EdgeError; #[derive(serde::Serialize)] struct User { @@ -165,8 +221,15 @@ struct User { } #[action] -async fn get_user() -> Json { - Json(User { id: 1, name: "Alice".into() }) +async fn get_user() -> Result { + let user = User { id: 1, name: "Alice".into() }; + let body = Body::json(&user).map_err(EdgeError::internal)?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(body) + .unwrap()) } ``` @@ -185,14 +248,19 @@ async fn not_found() -> (StatusCode, Text<&'static str>) { ### Custom Headers ```rust -use edgezero_core::http::{HeaderMap, HeaderValue}; -use edgezero_core::response::Text; +use edgezero_core::body::Body; +use edgezero_core::http::{HeaderValue, Response, StatusCode}; #[action] -async fn with_headers() -> (HeaderMap, Text<&'static str>) { - let mut headers = HeaderMap::new(); - headers.insert("x-custom", HeaderValue::from_static("value")); - (headers, Text::new("Response with custom header")) +async fn with_headers() -> Response { + let mut response = Response::builder() + .status(StatusCode::OK) + .body(Body::from("Response with custom header")) + .unwrap(); + response + .headers_mut() + .insert("x-custom", HeaderValue::from_static("value")); + response } ``` @@ -206,9 +274,8 @@ async fn update_user( Path(id): Path, Query(params): Query, Json(body): Json, -) -> Json { - // All three extractors are available - Json(User { id, name: body.name }) +) -> Text { + Text::new(format!("Updated user {} with name {}", id, body.name)) } ``` @@ -216,12 +283,12 @@ async fn update_user( Extractors return `EdgeError` on failure, which automatically converts to appropriate HTTP responses: -| Error | Status Code | -|-------|-------------| -| JSON parse error | 400 Bad Request | -| Validation error | 400 Bad Request | -| Missing path param | 500 Internal Server Error | -| Type conversion error | 400 Bad Request | +| Error | Status Code | +| --------------------- | ------------------------ | +| JSON parse error | 400 Bad Request | +| Validation error | 422 Unprocessable Entity | +| Missing path param | 400 Bad Request | +| Type conversion error | 400 Bad Request | For custom error handling, return `Result`: @@ -229,11 +296,92 @@ For custom error handling, return `Result`: use edgezero_core::error::EdgeError; #[action] -async fn fallible(Json(body): Json) -> Result, EdgeError> { +async fn fallible(Json(body): Json) -> Result, EdgeError> { if body.invalid { return Err(EdgeError::bad_request("Invalid request")); } - Ok(Json(Response { success: true })) + Ok(Text::new("Success")) +} +``` + +### EdgeError Methods + +`EdgeError` provides factory methods for common HTTP errors: + +```rust +use edgezero_core::error::EdgeError; + +// Client errors +EdgeError::bad_request("Invalid input") // 400 +EdgeError::not_found("/missing/path") // 404 +EdgeError::method_not_allowed(&method, &allowed) // 405 +EdgeError::validation("Field too short") // 422 + +// Server errors +EdgeError::internal("Unexpected failure") // 500 +EdgeError::internal(some_error) // 500 (from any error type) +``` + +## Custom Extractors + +Implement the `FromRequest` trait to create custom extractors: + +```rust +use async_trait::async_trait; +use edgezero_core::context::RequestContext; +use edgezero_core::error::EdgeError; +use edgezero_core::extractor::FromRequest; + +pub struct BearerToken(pub String); + +#[async_trait(?Send)] +impl FromRequest for BearerToken { + async fn from_request(ctx: &RequestContext) -> Result { + let header = ctx.request().headers() + .get("authorization") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| EdgeError::bad_request("Missing Authorization header"))?; + + let token = header + .strip_prefix("Bearer ") + .ok_or_else(|| EdgeError::bad_request("Invalid Bearer token format"))?; + + Ok(BearerToken(token.to_string())) + } +} + +// Use in handlers: +#[action] +async fn protected(BearerToken(token): BearerToken) -> Text { + Text::new(format!("Authenticated with token: {}...", &token[..8])) +} +``` + +## Custom Response Types + +Implement `IntoResponse` for custom response types: + +```rust +use edgezero_core::body::Body; +use edgezero_core::http::{Response, StatusCode}; +use edgezero_core::response::IntoResponse; + +pub struct HtmlResponse(pub String); + +impl IntoResponse for HtmlResponse { + fn into_response(self) -> Response { + Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/html; charset=utf-8") + .body(Body::from(self.0)) + .unwrap() + } +} + +// Use in handlers: +#[action] +async fn page() -> HtmlResponse { + HtmlResponse("

Hello

".to_string()) } ``` diff --git a/docs/guide/middleware.md b/docs/guide/middleware.md index 1c85f78..643827e 100644 --- a/docs/guide/middleware.md +++ b/docs/guide/middleware.md @@ -7,28 +7,37 @@ EdgeZero supports composable middleware for cross-cutting concerns like logging, Middleware implements the `Middleware` trait: ```rust -use edgezero_core::middleware::Middleware; -use edgezero_core::http::{Request, Response}; -use edgezero_core::body::Body; +use async_trait::async_trait; +use edgezero_core::context::RequestContext; +use edgezero_core::error::EdgeError; +use edgezero_core::http::Response; +use edgezero_core::middleware::{Middleware, Next}; pub struct RequestLogger; +#[async_trait(?Send)] impl Middleware for RequestLogger { async fn handle( &self, - req: Request, - next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, - ) -> Response { - let method = req.method().clone(); - let path = req.uri().path().to_string(); - - log::info!("--> {} {}", method, path); - - let response = next(req).await; - - log::info!("<-- {} {} {}", method, path, response.status()); - - response + ctx: RequestContext, + next: Next<'_>, + ) -> Result { + let method = ctx.request().method().clone(); + let path = ctx.request().uri().path().to_string(); + let start = std::time::Instant::now(); + + let response = next.run(ctx).await?; + + let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0; + tracing::info!( + "request method={} path={} status={} elapsed_ms={:.2}", + method, + path, + response.status().as_u16(), + elapsed_ms + ); + + Ok(response) } } ``` @@ -61,7 +70,7 @@ use edgezero_core::router::RouterService; let router = RouterService::builder() .middleware(RequestLogger) .middleware(CorsMiddleware::default()) - .route(Method::GET, "/hello", hello) + .get("/hello", hello) .build(); ``` @@ -86,26 +95,28 @@ pub struct AuthMiddleware { secret: String, } +#[async_trait(?Send)] impl Middleware for AuthMiddleware { async fn handle( &self, - req: Request, - next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, - ) -> Response { + ctx: RequestContext, + next: Next<'_>, + ) -> Result { // Check authorization header - let auth_header = req.headers().get("authorization"); - + let auth_header = ctx.request().headers().get("authorization"); + match auth_header { Some(value) if self.verify_token(value) => { // Token valid, continue to handler - next(req).await + next.run(ctx).await } _ => { // Return 401 Unauthorized - Response::builder() + let response = Response::builder() .status(401) .body(Body::from("Unauthorized")) - .unwrap() + .map_err(EdgeError::internal)?; + Ok(response) } } } @@ -119,19 +130,22 @@ pub struct CorsMiddleware { allowed_origins: Vec, } +#[async_trait(?Send)] impl Middleware for CorsMiddleware { async fn handle( &self, - req: Request, - next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, - ) -> Response { - let origin = req.headers() + ctx: RequestContext, + next: Next<'_>, + ) -> Result { + let origin = ctx + .request() + .headers() .get("origin") .and_then(|v| v.to_str().ok()) .map(String::from); - - let mut response = next(req).await; - + + let mut response = next.run(ctx).await?; + if let Some(origin) = origin { if self.allowed_origins.contains(&origin) { response.headers_mut().insert( @@ -140,8 +154,8 @@ impl Middleware for CorsMiddleware { ); } } - - response + + Ok(response) } } ``` @@ -151,23 +165,24 @@ impl Middleware for CorsMiddleware { ```rust pub struct TimingMiddleware; +#[async_trait(?Send)] impl Middleware for TimingMiddleware { async fn handle( &self, - req: Request, - next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, - ) -> Response { + ctx: RequestContext, + next: Next<'_>, + ) -> Result { let start = std::time::Instant::now(); - - let mut response = next(req).await; - + + let mut response = next.run(ctx).await?; + let duration = start.elapsed(); response.headers_mut().insert( "x-response-time", format!("{}ms", duration.as_millis()).parse().unwrap(), ); - - response + + Ok(response) } } ``` @@ -180,18 +195,19 @@ Middleware can short-circuit the chain by not calling `next`: impl Middleware for RateLimiter { async fn handle( &self, - req: Request, - next: impl FnOnce(Request) -> futures::future::BoxFuture<'static, Response> + Send, - ) -> Response { - if self.is_rate_limited(&req) { + ctx: RequestContext, + next: Next<'_>, + ) -> Result { + if self.is_rate_limited(&ctx) { // Don't call next - return immediately - return Response::builder() + let response = Response::builder() .status(429) .body(Body::from("Too Many Requests")) - .unwrap(); + .map_err(EdgeError::internal)?; + return Ok(response); } - - next(req).await + + next.run(ctx).await } } ``` @@ -200,8 +216,8 @@ impl Middleware for RateLimiter { EdgeZero provides these middleware out of the box: -| Middleware | Purpose | -|------------|---------| +| Middleware | Purpose | +| --------------- | ---------------------------------------------- | | `RequestLogger` | Logs request method, path, and response status | ## Next Steps diff --git a/docs/guide/proxying.md b/docs/guide/proxying.md index 2b3afb0..e6502f3 100644 --- a/docs/guide/proxying.md +++ b/docs/guide/proxying.md @@ -1,244 +1,63 @@ # Proxying -EdgeZero provides built-in helpers for forwarding requests to upstream services while staying provider-agnostic. +EdgeZero provides helpers for forwarding requests to upstream services while staying +provider-agnostic. -## Proxy Primitives +## End-to-End Example -The core proxy types live in `edgezero_core::proxy`: - -- **`ProxyRequest`** - Represents a request to forward upstream -- **`ProxyResponse`** - The response from the upstream service -- **`ProxyService`** - Executes proxy requests using a provider-specific client - -## Basic Proxying +This example forwards the incoming request upstream, adjusts headers on the way in and out, and +returns a friendly 502 on proxy errors. It uses the adapter-provided proxy handle inserted by each +adapter. ```rust use edgezero_core::action; -use edgezero_core::context::RequestContext; -use edgezero_core::http::{Response, Uri}; -use edgezero_core::proxy::{ProxyRequest, ProxyService}; use edgezero_core::body::Body; +use edgezero_core::context::RequestContext; +use edgezero_core::error::EdgeError; +use edgezero_core::http::{Response, StatusCode, Uri}; +use edgezero_core::proxy::ProxyRequest; #[action] -async fn proxy_to_api(RequestContext(ctx): RequestContext) -> Response { - let target: Uri = "https://api.example.com/v1".parse().unwrap(); - - // Build proxy request from incoming request - let proxy_request = ProxyRequest::from_request(ctx.into_request(), target); - - // Forward using the adapter's proxy client - let client = get_proxy_client(); // Platform-specific - let response = ProxyService::new(client) - .forward(proxy_request) - .await - .unwrap(); - - response.into_response() -} -``` - -## Adapter-Specific Clients - -Each adapter provides its own proxy client implementation: - -### Fastly - -```rust -use edgezero_adapter_fastly::FastlyProxyClient; - -let client = FastlyProxyClient::new("backend-name"); -let response = ProxyService::new(client).forward(request).await?; -``` - -The backend name must be configured in `fastly.toml`: - -```toml -[local_server.backends.backend-name] -url = "https://api.example.com" -``` - -### Cloudflare - -```rust -use edgezero_adapter_cloudflare::CloudflareProxyClient; - -let client = CloudflareProxyClient::new(); -let response = ProxyService::new(client).forward(request).await?; -``` - -Cloudflare Workers use the global `fetch` API. - -### Axum (Development) - -```rust -use edgezero_adapter_axum::AxumProxyClient; - -let client = AxumProxyClient::new(); -let response = ProxyService::new(client).forward(request).await?; -``` - -## Request Modification +async fn proxy_with_auth(RequestContext(ctx): RequestContext) -> Result { + let target: Uri = "https://api.example.com".parse().unwrap(); -Modify requests before forwarding: + let handle = ctx + .proxy_handle() + .ok_or_else(|| EdgeError::internal("proxy client not configured"))?; -```rust -#[action] -async fn proxy_with_auth(RequestContext(ctx): RequestContext) -> Response { - let target: Uri = "https://api.example.com".parse().unwrap(); - let mut proxy_request = ProxyRequest::from_request(ctx.into_request(), target); - - // Add authentication header proxy_request.headers_mut().insert( "authorization", "Bearer secret-token".parse().unwrap(), ); - - // Remove sensitive headers proxy_request.headers_mut().remove("cookie"); - - let client = get_proxy_client(); - ProxyService::new(client).forward(proxy_request).await?.into_response() -} -``` - -## Response Processing - -Process upstream responses before returning: - -```rust -#[action] -async fn proxy_with_transform(RequestContext(ctx): RequestContext) -> Response { - let target: Uri = "https://api.example.com".parse().unwrap(); - let proxy_request = ProxyRequest::from_request(ctx.into_request(), target); - - let client = get_proxy_client(); - let mut response = ProxyService::new(client).forward(proxy_request).await?; - - // Add cache headers - response.headers_mut().insert( - "cache-control", - "public, max-age=3600".parse().unwrap(), - ); - - // Add diagnostic header - response.headers_mut().insert( - "x-proxy-by", - "edgezero".parse().unwrap(), - ); - - response.into_response() -} -``` - -## Streaming Proxies - -Proxy requests preserve streaming bodies without buffering: - -```rust -// Large uploads/downloads stream through without loading into memory -let proxy_request = ProxyRequest::from_request(request, target); -let response = proxy.forward(proxy_request).await?; -// response.body is still streaming -``` - -## Transparent Decompression - -Proxied responses are automatically decompressed: - -| Content-Encoding | Handling | -|------------------|----------| -| `gzip` | Automatically decoded | -| `br` (brotli) | Automatically decoded | -| `identity` | Passed through | - -This allows you to process response bodies without manual decompression: - -```rust -let response = proxy.forward(request).await?; -let body = response.body().bytes().await?; -// body is already decompressed, ready for transformation -``` - -## Error Handling - -Proxy operations can fail for various reasons: -```rust -use edgezero_core::error::EdgeError; - -#[action] -async fn safe_proxy(RequestContext(ctx): RequestContext) -> Response { - let target: Uri = "https://api.example.com".parse().unwrap(); - let proxy_request = ProxyRequest::from_request(ctx.into_request(), target); - - let client = get_proxy_client(); - match ProxyService::new(client).forward(proxy_request).await { - Ok(response) => response.into_response(), - Err(e) => { - log::error!("Proxy failed: {}", e); - Response::builder() - .status(502) + match handle.forward(proxy_request).await { + Ok(mut response) => { + response + .headers_mut() + .insert("x-proxy-by", "edgezero".parse().unwrap()); + Ok(response) + } + Err(err) => { + tracing::error!("proxy failed: {}", err); + let response = Response::builder() + .status(StatusCode::BAD_GATEWAY) .body(Body::from("Bad Gateway")) - .unwrap() + .map_err(EdgeError::internal)?; + Ok(response) } } } ``` -## Common Use Cases - -### API Gateway +## Notes -```rust -#[action] -async fn api_gateway( - Path(service): Path, - RequestContext(ctx): RequestContext, -) -> Response { - let target = match service.as_str() { - "users" => "https://users-api.internal", - "orders" => "https://orders-api.internal", - _ => return Response::builder() - .status(404) - .body(Body::from("Service not found")) - .unwrap(), - }; - - let uri: Uri = target.parse().unwrap(); - let request = ProxyRequest::from_request(ctx.into_request(), uri); - - get_proxy_client() - .forward(request) - .await - .map(|r| r.into_response()) - .unwrap_or_else(|_| bad_gateway()) -} -``` - -### Caching Proxy - -```rust -#[action] -async fn caching_proxy(RequestContext(ctx): RequestContext) -> Response { - // Check cache first - if let Some(cached) = cache.get(ctx.uri().path()) { - return cached; - } - - // Proxy to origin - let response = proxy.forward(request).await?; - - // Cache successful responses - if response.status().is_success() { - cache.set(ctx.uri().path(), response.clone()); - } - - response.into_response() -} -``` +- Fastly and Cloudflare preserve streaming bodies; Axum buffers outbound bodies before sending. +- Fastly and Cloudflare automatically decode `gzip`/`br` responses for you. +- If you need a direct client (for tests or custom wiring), use the adapter clients + (`FastlyProxyClient`, `CloudflareProxyClient`, `AxumProxyClient::default()`). ## Next Steps -- Configure backends in [Fastly](/guide/adapters/fastly) adapter guide -- Learn about [Cloudflare](/guide/adapters/cloudflare) fetch integration +- Learn about [Fastly](/guide/adapters/fastly) and [Cloudflare](/guide/adapters/cloudflare) adapter specifics diff --git a/docs/guide/roadmap.md b/docs/guide/roadmap.md new file mode 100644 index 0000000..1cece10 --- /dev/null +++ b/docs/guide/roadmap.md @@ -0,0 +1,39 @@ +# Roadmap + +This page captures upcoming EdgeZero work and longer-term bets. Items here are directional and may +shift as the roadmap evolves. + +## Roadmap (2025-09-24) + +- Adapter stability: formalise the provider adapter contract (request/response mapping, streaming + guarantees, proxy hooks) and capture it in shared docs + integration tests so new targets plug in + safely. +- Provider additions: prototype a third adapter (e.g. AWS Lambda@Edge or Vercel Edge Functions) + using the stabilized adapter API to validate cross-provider abstractions. +- Manifest ergonomics: evolve `edgezero.toml` to mirror Spin’s manifest convenience (route triggers, + env/secrets, build targets) while remaining provider-agnostic; update CLI scaffolding accordingly. +- Tooling parity: extend `edgezero-cli` with template/plugin style commands (similar to Spin + templates) to streamline new app scaffolds and provider-specific wiring. +- CLI parity backlog: add `edgezero --list-adapters`, standardize exit codes, search up for + `edgezero.toml`, respect `RUST_LOG` for dev output, and bake in hot reload for `edgezero dev`. +- Documentation parity: publish a single-source-of-truth docs set aligned with current APIs + (App::build_app entrypoints, adapter dispatch signatures, middleware signature, proxy handle + usage) and keep it in sync with code changes. +- Example coverage: add focused guides for `axum.toml`, manifest `description` fields, logging + precedence, and route listing + body-mode behavior to reduce ambiguity. +- Adapter contract alignment: document which adapters buffer bodies, which preserve streaming, and + where proxy headers/automatic decompression apply so expectations match runtime behavior. +- Spin support: add first-class Spin adapter support and document how EdgeZero manifests map to + Spin-compatible deployments. + +## Open Design Questions (for later pickup) + +- Provider priorities: focus on Fastly Compute@Edge, then Cloudflare Workers. +- Minimum Rust version (MSRV) target. +- Async story: keep core sync or introduce async features (Tokio) behind flags? +- Request/Response mapping rules (header casing, multi-value headers, binary bodies). +- Caching/edge-specific headers: how much to standardize (e.g., Surrogate-Control)? +- Dev UX: integrate a local hyper server behind a feature vs. keeping zero-deps TCP server. +- Packaging/deploy: preferred tooling (Fastly CLI/API; AWS SAM/CDK or native Lambda tooling). +- Config format: TOML/JSON/YAML; env overlays. +- License and contribution guidelines. diff --git a/docs/guide/routing.md b/docs/guide/routing.md index 81470e1..98649ee 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -20,16 +20,28 @@ methods = ["GET", "POST"] handler = "my_app_core::handlers::echo" ``` -You can also build routes programmatically: +You can also build routes programmatically using convenience methods: + +```rust +use edgezero_core::router::RouterService; + +let router = RouterService::builder() + .get("/hello", hello_handler) + .get("/echo/{name}", echo_handler) + .post("/echo", echo_json_handler) + .build(); +``` + +Or with explicit method specification: ```rust use edgezero_core::router::RouterService; use edgezero_core::http::Method; let router = RouterService::builder() - .route(Method::GET, "/hello", hello_handler) - .route(Method::GET, "/echo/{name}", echo_handler) - .route(Method::POST, "/echo", echo_json_handler) + .route("/hello", Method::GET, hello_handler) + .route("/echo/{name}", Method::GET, echo_handler) + .route("/echo", Method::POST, echo_json_handler) .build(); ``` @@ -91,11 +103,12 @@ handler = "my_app_core::handlers::resource" Or programmatically: ```rust -router - .route(Method::GET, "/resource", get_resource) - .route(Method::POST, "/resource", create_resource) - .route(Method::PUT, "/resource/{id}", update_resource) - .route(Method::DELETE, "/resource/{id}", delete_resource) +RouterService::builder() + .get("/resource", get_resource) + .post("/resource", create_resource) + .put("/resource/{id}", update_resource) + .delete("/resource/{id}", delete_resource) + .build() ``` EdgeZero automatically returns `405 Method Not Allowed` for requests that match a path but use an unsupported method. @@ -107,7 +120,7 @@ Enable route listing for debugging: ```rust let router = RouterService::builder() .enable_route_listing() - .route(Method::GET, "/hello", hello) + .get("/hello", hello) .build(); ``` @@ -131,10 +144,10 @@ RouterService::builder() EdgeZero uses matchit's path syntax: -| Pattern | Example | Matches | -|---------|---------|---------| -| `/static` | `/static` | Exact match only | -| `/{param}` | `/users/{id}` | Single segment: `/users/123` | +| Pattern | Example | Matches | +| ----------- | ---------------- | ---------------------------- | +| `/static` | `/static` | Exact match only | +| `/{param}` | `/users/{id}` | Single segment: `/users/123` | | `/{*catch}` | `/files/{*path}` | Rest of path: `/files/a/b/c` | ::: warning Legacy Syntax @@ -143,19 +156,9 @@ Axum-style `:name` parameters are **not supported**. Use `{name}` instead. ## Route Priority -Routes are matched in registration order. More specific routes should be registered before catch-alls: - -```rust -// Good: specific route first -router - .route(Method::GET, "/users/me", get_current_user) - .route(Method::GET, "/users/{id}", get_user_by_id) - -// Bad: catch-all shadows specific routes -router - .route(Method::GET, "/users/{id}", get_user_by_id) - .route(Method::GET, "/users/me", get_current_user) // Never reached! -``` +Routes are matched by specificity (static segments first, then parameters, then catch-alls). If two +routes have the same specificity, the first registered wins. Avoid ambiguous patterns that share +the same shape (for example, two routes that both look like `/users/{id}`). ## Next Steps diff --git a/docs/guide/streaming.md b/docs/guide/streaming.md index c4523ac..6305178 100644 --- a/docs/guide/streaming.md +++ b/docs/guide/streaming.md @@ -10,18 +10,19 @@ Use `Body::stream` to yield response chunks progressively: use edgezero_core::action; use edgezero_core::body::Body; use edgezero_core::http::Response; +use bytes::Bytes; use futures::stream; #[action] -async fn stream_data() -> Response { +async fn stream_data() -> Response { let chunks = vec![ - Ok::<_, std::io::Error>(vec![b'H', b'e', b'l', b'l', b'o']), - Ok(vec![b' ']), - Ok(vec![b'W', b'o', b'r', b'l', b'd']), + Bytes::from_static(b"Hello"), + Bytes::from_static(b" "), + Bytes::from_static(b"World"), ]; - + let body = Body::stream(stream::iter(chunks)); - + Response::builder() .status(200) .header("content-type", "text/plain") @@ -44,19 +45,17 @@ The router keeps streams intact through the adapter layer: Stream events to clients with SSE: ```rust -use futures::stream::StreamExt; +use bytes::Bytes; #[action] -async fn events() -> Response { +async fn events() -> Response { let events = async_stream::stream! { for i in 0..10 { - yield Ok::<_, std::io::Error>( - format!("data: Event {}\n\n", i).into_bytes() - ); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let payload = format!("data: Event {}\n\n", i); + yield Bytes::from(payload); } }; - + Response::builder() .status(200) .header("content-type", "text/event-stream") @@ -68,7 +67,8 @@ async fn events() -> Response { ## Body Modes -Routes can specify their body handling mode in the manifest: +Routes can specify their body handling mode in the manifest. This is parsed today and reserved +for future enforcement by adapters and router helpers: ```toml [[triggers.http]] @@ -78,10 +78,10 @@ handler = "my_app::handlers::upload" body-mode = "buffered" # or "stream" ``` -| Mode | Behavior | -|------|----------| -| `buffered` | Body is fully read into memory before handler runs | -| `stream` | Body is passed as a stream for progressive processing | +| Mode | Behavior | +| ---------- | ----------------------------------------------------- | +| `buffered` | Body is fully read into memory before handler runs | +| `stream` | Body is passed as a stream for progressive processing | ## Transparent Decompression @@ -114,9 +114,9 @@ When the response size is unknown, EdgeZero uses chunked transfer encoding: ```rust #[action] -async fn dynamic_content() -> Response { +async fn dynamic_content() -> Response { let stream = generate_content_stream(); - + // No Content-Length header needed Response::builder() .status(200) diff --git a/docs/guide/what-is-edgezero.md b/docs/guide/what-is-edgezero.md index 38f7949..3583d69 100644 --- a/docs/guide/what-is-edgezero.md +++ b/docs/guide/what-is-edgezero.md @@ -22,17 +22,18 @@ EdgeZero separates your application into layers: 3. **Entrypoints** - Minimal main functions that wire the adapter to your core app This architecture means you can: + - Develop locally with the Axum adapter's dev server - Test your handlers in isolation without provider SDKs - Deploy the same logic to multiple edge platforms ## Supported Platforms -| Platform | Target | Status | -|----------|--------|--------| -| Fastly Compute@Edge | `wasm32-wasip1` | Stable | -| Cloudflare Workers | `wasm32-unknown-unknown` | Stable | -| Axum/Tokio (native) | Native host | Stable | +| Platform | Target | Status | +| ------------------- | ------------------------ | ------ | +| Fastly Compute@Edge | `wasm32-wasip1` | Stable | +| Cloudflare Workers | `wasm32-unknown-unknown` | Stable | +| Axum/Tokio (native) | Native host | Stable | ## Use Cases diff --git a/docs/index.md b/docs/index.md index 8e3b8bb..96650af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,9 +2,9 @@ layout: home hero: - name: "EdgeZero" - text: "Write Once, Deploy Everywhere" - tagline: "Production-ready toolkit for portable edge HTTP workloads" + name: 'EdgeZero' + text: 'Write Once, Deploy Everywhere' + tagline: 'Production-ready toolkit for portable edge HTTP workloads' actions: - theme: brand text: Get Started @@ -16,14 +16,14 @@ hero: features: - title: Write Once, Deploy Anywhere details: Build your HTTP workload once with runtime-agnostic core code that compiles to WebAssembly or native targets without changes. - - title: Fastly Compute Support + - title: Fastly Compute@Edge Support details: Deploy to Fastly Compute@Edge with zero-cold-start WASM binaries using the wasm32-wasip1 target. - title: Cloudflare Workers Support details: Run on Cloudflare Workers with seamless wrangler integration and wasm32-unknown-unknown compilation. - title: Native Development (Axum) details: Develop locally with a full-featured Axum/Tokio dev server, then deploy to containers or native hosts. - title: Type-Safe Extractors - details: Use ergonomic extractors like Json, Path, and ValidatedQuery with the #[action] macro for clean handler code. + details: 'Use ergonomic extractors like Json, Path, Query, and validated variants with the #[action] macro for clean handler code' - title: Streaming & Proxying details: Stream responses progressively with Body::stream and forward traffic upstream with built-in proxy helpers. --- diff --git a/docs/package-lock.json b/docs/package-lock.json index 995f729..b3f44e2 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9,6 +9,11 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/node": "^24.10", + "eslint": "^9.17.0", + "prettier": "^3.4.2", + "typescript-eslint": "^8.18.2", "vitepress": "^1.5.0" } }, @@ -762,6 +767,202 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.68", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.68.tgz", @@ -1240,6 +1441,13 @@ "@types/unist": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -1275,6 +1483,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1289,6 +1507,262 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "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", @@ -1561,6 +2035,46 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/algoliasearch": { "version": "5.47.0", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.47.0.tgz", @@ -1587,16 +2101,67 @@ "node": ">= 14.0.0" } }, - "node_modules/birpc": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", - "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1608,6 +2173,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", @@ -1630,6 +2212,26 @@ "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", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1641,6 +2243,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -1657,6 +2266,21 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1664,6 +2288,31 @@ "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1747,6 +2396,163 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -1754,6 +2560,106 @@ "dev": true, "license": "MIT" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/focus-trap": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", @@ -1779,6 +2685,42 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -1835,6 +2777,66 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-what": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", @@ -1848,6 +2850,94 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1981,6 +3071,19 @@ ], "license": "MIT" }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minisearch": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", @@ -1995,6 +3098,13 @@ "dev": true, "license": "MIT" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2014,6 +3124,13 @@ "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": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", @@ -2026,6 +3143,89 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -2040,6 +3240,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2080,6 +3293,32 @@ "url": "https://opencollective.com/preact" } }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -2091,6 +3330,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", @@ -2118,6 +3367,16 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -2178,6 +3437,42 @@ "license": "MIT", "peer": true }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shiki": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", @@ -2241,6 +3536,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -2254,6 +3562,19 @@ "node": ">=16" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tabbable": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", @@ -2261,6 +3582,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -2272,6 +3610,78 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -2345,6 +3755,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -2499,6 +3919,45 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/docs/package.json b/docs/package.json index e179898..559e652 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,9 +7,18 @@ "scripts": { "dev": "vitepress dev", "build": "vitepress build", - "preview": "vitepress preview" + "preview": "vitepress preview", + "format": "prettier --check .", + "format:write": "prettier --write .", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "devDependencies": { + "@types/node": "^24.10", + "eslint": "^9.17.0", + "@eslint/js": "^9.17.0", + "typescript-eslint": "^8.18.2", + "prettier": "^3.4.2", "vitepress": "^1.5.0" } } From 083af9ed38b8f608fdc66cae21b5860e7326cf22 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:53:15 -0800 Subject: [PATCH 3/3] Fixed typos --- README.md | 2 +- TODO.md | 51 +++++++++++++++++++++++++++++++ docs/guide/adapters/cloudflare.md | 14 ++++----- docs/guide/architecture.md | 2 +- docs/guide/cli-reference.md | 18 +++++------ docs/guide/configuration.md | 8 ++++- docs/guide/handlers.md | 22 +++++++++++++ docs/guide/middleware.md | 4 +++ docs/guide/roadmap.md | 44 +++++++++++++------------- docs/guide/streaming.md | 3 ++ 10 files changed, 125 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index bd79dd0..8431ad7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Production-ready toolkit for portable edge HTTP workloads. Write once, deploy to cargo install --path crates/edgezero-cli # Create a new project -edgezero new my-app --adapters fastly cloudflare axum +edgezero new my-app cd my-app # Start the dev server diff --git a/TODO.md b/TODO.md index df4b60f..b9131ca 100644 --- a/TODO.md +++ b/TODO.md @@ -527,6 +527,57 @@ High-level backlog and decisions to drive the next milestones. - Assumptions: The new roadmap bullets are directional and do not imply implementation order. - Outstanding: None (docs-only updates). +## Codex Plan (2026-01-27 - Post-Merge Docs Review) + +- [x] Diff `main` vs current branch to scope doc-related changes post-merge. +- [x] Re-review docs for redundancy, inconsistencies, missing coverage, thoroughness, and verbosity. +- [x] Summarize findings with file references and prioritized issues. + +## Review (2026-01-27 01:40:07 UTC) + +- Summary: Re-ran the post-merge documentation audit against `main`, flagging the top doc/behavior mismatches (CLI flags, trigger adapter filtering, Fastly backend wording) plus missing manifest field coverage and CLI passthrough docs. +- Assumptions: Review targets current runtime behavior; undocumented manifest fields and adapter filters should be explicitly labeled as reserved/ignored if they remain unimplemented. +- Outstanding: None (review-only; no code changes applied). + +## Codex Plan (2026-01-27 - Docs Fixes After Review) + +- [x] Align CLI usage docs with actual flags (`edgezero new`, passthrough args) and remove/replace unsupported examples. +- [x] Reconcile adapter/proxy docs (Cloudflare vs Fastly outbound request wording; adapter filtering behavior). +- [x] Document or mark reserved manifest fields (`app.version`, `app.kind`) and trim duplicate CLI install instructions. +- [x] Summarize changes with file references in TODO review entry. + +## Review (2026-01-27 01:58:05 UTC) + +- Summary: Aligned CLI and configuration docs with actual flags and manifest behavior, clarified Fastly/Cloudflare outbound request wording, and trimmed duplicate CLI install guidance; captured test failures in `debug.md`. +- Assumptions: Manifest `version` and `kind` remain reserved fields until the parser is extended; adapter filters stay informational unless `app!` adds enforcement. +- Outstanding: `cargo test` failed in `edgezero-adapter-axum` integration tests due to local port bind permissions (see `debug.md`). + +## Review (2026-01-27 02:13:01 UTC) + +- Summary: Cleaned the roadmap to focus on forward-looking items, moved completed work into a "Completed (Recent)" section, removed the stale date header, and tightened Spin wording to "mirror" rather than "match." +- Assumptions: The completed items reflect the current docs/tests baseline; future roadmap entries should be updated as features land. +- Outstanding: `cargo test` failed in `edgezero-adapter-axum` integration tests due to local port bind permissions (see `debug.md`). + +## Review (2026-01-27 02:17:12 UTC) + +- Summary: Pruned the open design questions to only unresolved items, and moved resolved topics (platform focus, adapter mapping rules) into the completed section. +- Assumptions: Remaining open questions reflect genuine policy decisions rather than already-shipped behavior. +- Outstanding: `cargo test` failed in `edgezero-adapter-axum` integration tests due to local port bind permissions (see `debug.md`). + +## Codex Plan (2026-01-27 - Roadmap Cleanup) + +- [x] Audit `docs/guide/roadmap.md` for redundancy, stale dating, and overlapping bullets. +- [x] Restructure the roadmap into clearer groupings (e.g., near-term vs later, or by theme) and tighten wording. +- [x] Keep Spin-related intent but ensure it reads as “mirrors” and does not overpromise; remove any duplicate items. +- [x] Update TODO review entry with summary and file references. + +## Codex Plan (2026-01-27 - Open Design Questions Cleanup) + +- [x] Review `docs/guide/roadmap.md` "Open Design Questions" and identify which are already resolved. +- [x] Remove resolved items or move them into "Completed (Recent)" with brief phrasing. +- [x] Keep only genuinely open questions, tightening wording to avoid duplication. +- [x] Update TODO review entry with summary and file references. + ## Codex Plan (2026-01-27 - Roadmap Doc Page) - [x] Add a dedicated roadmap page under `docs/guide/roadmap.md`. diff --git a/docs/guide/adapters/cloudflare.md b/docs/guide/adapters/cloudflare.md index 00e165a..a71ce60 100644 --- a/docs/guide/adapters/cloudflare.md +++ b/docs/guide/adapters/cloudflare.md @@ -193,13 +193,13 @@ Configure the Cloudflare adapter in `edgezero.toml`. See [Configuration](/guide/ ## Comparison with Fastly -| Feature | Cloudflare Workers | Fastly Compute | -| ----------------- | ------------------------ | ---------------------- | -| Target | `wasm32-unknown-unknown` | `wasm32-wasip1` | -| Outbound requests | Global `fetch` | Named backends | -| Storage | KV, Durable Objects, R2 | KV Store, Object Store | -| Logging | `console.log` | Log endpoints | -| CLI | Wrangler | Fastly CLI | +| Feature | Cloudflare Workers | Fastly Compute | +| ----------------- | ------------------------ | ----------------------------------- | +| Target | `wasm32-unknown-unknown` | `wasm32-wasip1` | +| Outbound requests | Global `fetch` | Dynamic backends (derived from URI) | +| Storage | KV, Durable Objects, R2 | KV Store, Object Store | +| Logging | `console.log` | Log endpoints | +| CLI | Wrangler | Fastly CLI | ## Next Steps diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md index 2ca1411..50599a7 100644 --- a/docs/guide/architecture.md +++ b/docs/guide/architecture.md @@ -25,7 +25,7 @@ edgezero/ - **Routing** - `RouterService` with path parameter matching via `matchit` - **Request/Response** - Portable `http::Request` and `http::Response` types - **Body** - Unified body type supporting buffered and streaming modes -- **Extractors** - `Json`, `Path`, `Query`, `Form`, `Headers`, and `Validated*` variants +- **Extractors** - `Json`, `Path`, `Query`, `Form`, `Headers`, `Host`, `ForwardedHost`, and `Validated*` variants - **Middleware** - Composable middleware chain with async support - **Manifest** - `edgezero.toml` parsing and validation - **Compression** - Shared gzip/brotli stream decoders diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index 6247ddb..0ff70db 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -4,17 +4,7 @@ The `edgezero` CLI provides commands for scaffolding, development, building, and ## Installation -Install from the workspace: - -```bash -cargo install --path crates/edgezero-cli -``` - -Or from a published crate: - -```bash -cargo install edgezero-cli -``` +Follow the [Getting Started](/guide/getting-started) guide to install the CLI. ## Commands @@ -105,6 +95,12 @@ edgezero build --adapter axum The command executes the `build` command from `[adapters..commands]` in `edgezero.toml`, or falls back to the built-in adapter helper. +Any arguments after `--` are forwarded to the adapter command: + +```bash +edgezero build --adapter fastly -- --flag value +``` + ### edgezero serve Run the provider-specific local server: diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 6d58288..c0d9275 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -40,6 +40,8 @@ middleware = ["edgezero_core::middleware::RequestLogger"] | ------------ | -------- | -------------------------------------------------------------------- | | `name` | No | Display name for the application (defaults to "EdgeZero App") | | `entry` | No | Path to the core crate containing handlers (recommended for tooling) | +| `version` | No | Reserved for future compatibility; currently ignored | +| `kind` | No | Reserved for future compatibility; currently ignored | | `middleware` | No | List of middleware to apply globally | ### Middleware @@ -86,10 +88,14 @@ body-mode = "buffered" | `path` | Yes | URI template (`{param}` for params, `{*rest}` for catch-all) | | `methods` | No | Allowed HTTP methods (defaults to `GET`) | | `handler` | No | Path to handler function (required for `app!` route wiring) | -| `adapters` | No | Which adapters expose this route (empty = all) | +| `adapters` | No | Intended adapter filter (metadata; `app!` currently ignores) | | `description` | No | Human-readable description for docs or tooling | | `body-mode` | No | `buffered` or `stream` | +::: tip Adapter filters +The `adapters` field is currently metadata for tooling; `app!` wires all triggers regardless of adapter. +::: + ## Environment Section Declare environment variables and secrets: diff --git a/docs/guide/handlers.md b/docs/guide/handlers.md index 0d735b7..f13e4ef 100644 --- a/docs/guide/handlers.md +++ b/docs/guide/handlers.md @@ -159,6 +159,27 @@ async fn submit_form(Form(data): Form) -> Text { Use `ValidatedForm` for form data with validation, and `ValidatedPath` for validated path parameters. +### Host Extractors + +Extract the hostname from request headers: + +```rust +use edgezero_core::extractor::{Host, ForwardedHost}; + +// Extract from the Host header (falls back to "localhost") +#[action] +async fn check_host(Host(host): Host) -> Text { + Text::new(format!("Host: {}", host)) +} + +// Extract from X-Forwarded-Host first, then Host header +// Use this when behind a reverse proxy or load balancer +#[action] +async fn check_forwarded(ForwardedHost(host): ForwardedHost) -> Text { + Text::new(format!("Effective host: {}", host)) +} +``` + ### Request Context For full request access, handlers can receive `RequestContext` directly (no `#[action]` needed): @@ -190,6 +211,7 @@ async fn inspect(ctx: RequestContext) -> Result, EdgeError> { | `json::()` | Deserialize JSON body to `T` | | `form::()` | Deserialize form body to `T` | | `body()` | `&Body` - raw request body | +| `into_request()` | `Request` - consume context, take request | | `proxy_handle()` | `Option` - adapter proxy hook | ## Response Types diff --git a/docs/guide/middleware.md b/docs/guide/middleware.md index 643827e..a038ac3 100644 --- a/docs/guide/middleware.md +++ b/docs/guide/middleware.md @@ -91,6 +91,8 @@ Response Flow: ### Authentication ```rust +use edgezero_core::body::Body; + pub struct AuthMiddleware { secret: String, } @@ -192,6 +194,8 @@ impl Middleware for TimingMiddleware { Middleware can short-circuit the chain by not calling `next`: ```rust +use edgezero_core::body::Body; + impl Middleware for RateLimiter { async fn handle( &self, diff --git a/docs/guide/roadmap.md b/docs/guide/roadmap.md index 1cece10..e5ce1b8 100644 --- a/docs/guide/roadmap.md +++ b/docs/guide/roadmap.md @@ -3,37 +3,37 @@ This page captures upcoming EdgeZero work and longer-term bets. Items here are directional and may shift as the roadmap evolves. -## Roadmap (2025-09-24) +## Near-Term Priorities -- Adapter stability: formalise the provider adapter contract (request/response mapping, streaming - guarantees, proxy hooks) and capture it in shared docs + integration tests so new targets plug in - safely. -- Provider additions: prototype a third adapter (e.g. AWS Lambda@Edge or Vercel Edge Functions) - using the stabilized adapter API to validate cross-provider abstractions. -- Manifest ergonomics: evolve `edgezero.toml` to mirror Spin’s manifest convenience (route triggers, - env/secrets, build targets) while remaining provider-agnostic; update CLI scaffolding accordingly. - Tooling parity: extend `edgezero-cli` with template/plugin style commands (similar to Spin templates) to streamline new app scaffolds and provider-specific wiring. - CLI parity backlog: add `edgezero --list-adapters`, standardize exit codes, search up for `edgezero.toml`, respect `RUST_LOG` for dev output, and bake in hot reload for `edgezero dev`. -- Documentation parity: publish a single-source-of-truth docs set aligned with current APIs - (App::build_app entrypoints, adapter dispatch signatures, middleware signature, proxy handle - usage) and keep it in sync with code changes. +- Adapter behavior matrix: document which adapters buffer bodies, which preserve streaming, and + where proxy headers/automatic decompression apply so expectations match runtime behavior. - Example coverage: add focused guides for `axum.toml`, manifest `description` fields, logging precedence, and route listing + body-mode behavior to reduce ambiguity. -- Adapter contract alignment: document which adapters buffer bodies, which preserve streaming, and - where proxy headers/automatic decompression apply so expectations match runtime behavior. -- Spin support: add first-class Spin adapter support and document how EdgeZero manifests map to +- Spin support: add first-class Spin adapter support and document how EdgeZero manifests mirror Spin-compatible deployments. +- Provider additions: prototype a third adapter (e.g. AWS Lambda@Edge or Vercel Edge Functions) + using the stabilized adapter API to validate cross-provider abstractions. + +## Completed (Recent) + +- Adapter stability: formalised the provider adapter contract and shipped shared docs + integration + tests so new targets plug in safely. +- Manifest ergonomics: established the `edgezero.toml` schema and CLI scaffolding for route + triggers, env/secrets, and build targets. +- Documentation baseline: published a single-source-of-truth docs set aligned with current APIs + (App::build_app entrypoints, adapter dispatch signatures, middleware signature, proxy handle + usage). +- Platform focus: Fastly Compute@Edge and Cloudflare Workers are the primary edge targets, with Axum + serving local development and native deployment needs. +- Core contracts: request/response mapping rules are now captured in the adapter contract docs. ## Open Design Questions (for later pickup) -- Provider priorities: focus on Fastly Compute@Edge, then Cloudflare Workers. -- Minimum Rust version (MSRV) target. -- Async story: keep core sync or introduce async features (Tokio) behind flags? -- Request/Response mapping rules (header casing, multi-value headers, binary bodies). +- Minimum Rust version (MSRV) target and upgrade cadence. - Caching/edge-specific headers: how much to standardize (e.g., Surrogate-Control)? -- Dev UX: integrate a local hyper server behind a feature vs. keeping zero-deps TCP server. -- Packaging/deploy: preferred tooling (Fastly CLI/API; AWS SAM/CDK or native Lambda tooling). -- Config format: TOML/JSON/YAML; env overlays. -- License and contribution guidelines. +- Config overlays: strategy for environment-specific overrides in `edgezero.toml`. +- Contribution guidelines and governance model. diff --git a/docs/guide/streaming.md b/docs/guide/streaming.md index 6305178..b0792a4 100644 --- a/docs/guide/streaming.md +++ b/docs/guide/streaming.md @@ -45,6 +45,9 @@ The router keeps streams intact through the adapter layer: Stream events to clients with SSE: ```rust +use edgezero_core::action; +use edgezero_core::body::Body; +use edgezero_core::http::Response; use bytes::Bytes; #[action]