diff --git a/docs/api_examples.md b/docs/api_examples.md index 2f458e1..3b02d10 100644 --- a/docs/api_examples.md +++ b/docs/api_examples.md @@ -1,5 +1,5 @@ ```bash -curl -X POST http://localhost:8080/sleep \ +curl -X POST http://localhost:8080/api/sleep \ -H "Content-Type: application/json" \ -d '{"date":"2025-06-17","bed_time":"23:05","wake_time":"06:15","latency_min":10,"awakenings":1,"quality":4}' ``` diff --git a/openapi.yaml b/openapi.yaml index 87a71d3..a5cbdf4 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3,7 +3,7 @@ info: title: Sleep API version: '0.1' paths: - /login: + /api/login: post: summary: Login (form) description: Issues session and CSRF cookies via Set-Cookie. Send subsequent mutating requests with X-CSRF-Token equal to the CSRF cookie value. @@ -22,7 +22,7 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - /login.json: + /api/login.json: post: summary: Login (JSON) deprecated: true @@ -43,7 +43,7 @@ paths: properties: ok: type: boolean - /logout: + /api/logout: post: summary: Logout security: @@ -64,7 +64,7 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - /health: + /api/health: get: responses: '200': @@ -89,7 +89,7 @@ paths: authenticated: type: boolean - /sleep: + /api/sleep: post: requestBody: required: true @@ -122,7 +122,7 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - /sleep/date/{date}: + /api/sleep/date/{date}: get: parameters: - in: path @@ -147,7 +147,30 @@ paths: $ref: '#/components/schemas/Error' '404': description: Not Found - /sleep/{id}: + /api/sleep/{id}: + get: + parameters: + - in: path + name: id + schema: + type: integer + security: + - cookieAuth: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SleepSession' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found put: parameters: - in: path @@ -202,7 +225,80 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - /exercise: + /api/sleep/recent: + get: + summary: Recent daily sleep entries + parameters: + - in: query + name: days + required: false + schema: + type: integer + minimum: 1 + maximum: 31 + security: + - cookieAuth: [] + responses: + '200': + description: Up to N recent entries ordered desc by date + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SleepListItem' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/sleep/range: + get: + summary: Daily sleep entries within an inclusive date range + parameters: + - in: query + name: from + required: true + schema: + type: string + format: date + - in: query + name: to + required: true + schema: + type: string + format: date + security: + - cookieAuth: [] + responses: + '200': + description: Entries in range ordered asc by date + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SleepListItem' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/exercise: post: requestBody: required: true @@ -235,7 +331,46 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - /note: + /api/exercise/intensity: + get: + summary: Exercise intensity by date in range + parameters: + - in: query + name: from + required: true + schema: + type: string + format: date + - in: query + name: to + required: true + schema: + type: string + format: date + security: + - cookieAuth: [] + responses: + '200': + description: Intensities ordered asc by date + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DateIntensity' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/note: post: requestBody: required: true @@ -443,6 +578,15 @@ components: duration_min: type: integer nullable: true + DateIntensity: + type: object + properties: + date: + type: string + format: date + intensity: + type: string + enum: [none, light, hard] NoteInput: type: object properties: @@ -462,8 +606,40 @@ components: Error: type: object properties: - error: + code: + type: string + message: type: string detail: type: string nullable: true + SleepListItem: + type: object + properties: + id: + type: integer + date: + type: string + format: date + bed_time: + type: string + format: time + wake_time: + type: string + format: time + latency_min: + type: integer + awakenings: + type: integer + quality: + type: integer + duration_min: + type: integer + nullable: true + BadRequest: + type: object + properties: + code: + type: string + message: + type: string diff --git a/sleep-api/src/app.rs b/sleep-api/src/app.rs index 921d04b..381d38b 100644 --- a/sleep-api/src/app.rs +++ b/sleep-api/src/app.rs @@ -26,7 +26,7 @@ use axum::response::{Html, IntoResponse, Redirect}; use axum::{ Json, Router, extract::{Form, Path, State}, - routing::{get, post, put}, + routing::{get, post}, }; use axum_extra::extract::cookie::{Cookie, Key, PrivateCookieJar, SameSite}; use serde_json::json; @@ -34,18 +34,18 @@ use serde_json::json; #[doc = r#"Build the application [`Router`]. Routes: -- `GET /health` -- `HEAD /health` -- `POST /login` -- `POST /login.json` -- `POST /logout` +- `GET /api/health` +- `HEAD /api/health` +- `POST /api/login` +- `POST /api/login.json` +- `POST /api/logout` - `GET /api/session` -- `POST /sleep` -- `GET /sleep/date/{date}` -- `PUT /sleep/{id}` -- `DELETE /sleep/{id}` -- `POST /exercise` -- `POST /note` +- `POST /api/sleep` +- `GET /api/sleep/date/{date}` +- `PUT /api/sleep/{id}` +- `DELETE /api/sleep/{id}` +- `POST /api/exercise` +- `POST /api/note` - `GET /api/trends/sleep-bars` - `GET /api/trends/summary` @@ -117,16 +117,22 @@ pub fn router(db: Db) -> Router { }; let router = Router::new() .route("/", get(root)) - .route("/health", get(health_get).head(health_head)) - .route("/login", post(post_login)) - .route("/login.json", post(post_login_json)) - .route("/logout", post(post_logout)) + .route("/api/health", get(health_get).head(health_head)) + .route("/api/login", post(post_login)) + .route("/api/login.json", post(post_login_json)) + .route("/api/logout", post(post_logout)) .route("/api/session", get(api_session)) - .route("/sleep", post(create_sleep)) - .route("/sleep/date/{date}", get(get_sleep)) - .route("/sleep/{id}", put(update_sleep).delete(delete_sleep)) - .route("/exercise", post(create_exercise)) - .route("/note", post(create_note)) + .route("/api/sleep", post(create_sleep)) + .route("/api/sleep/date/{date}", get(get_sleep)) + // Register methods for /api/sleep/{id} explicitly to avoid any chaining ambiguity + .route("/api/sleep/{id}", get(get_sleep_by_id)) + .route("/api/sleep/{id}", axum::routing::put(update_sleep)) + .route("/api/sleep/{id}", axum::routing::delete(delete_sleep)) + .route("/api/sleep/recent", get(get_sleep_recent)) + .route("/api/sleep/range", get(get_sleep_range)) + .route("/api/exercise", post(create_exercise)) + .route("/api/exercise/intensity", get(get_exercise_intensity)) + .route("/api/note", post(create_note)) .route("/api/trends/sleep-bars", get(trends::sleep_bars)) .route("/api/trends/summary", get(trends::summary)) .with_state(state); @@ -161,7 +167,7 @@ async fn root() -> StatusCode { #[doc = r#"Login (form) and issue session + CSRF cookies. -Accepts: `POST /login` (`application/x-www-form-urlencoded`) +Accepts: `POST /api/login` (`application/x-www-form-urlencoded`) - Body: `{ email, password }` - On success: - Issues encrypted session cookie (see [`crate::config::session_cookie_name`]) @@ -178,7 +184,7 @@ Responses: Example: ```bash -curl -i -X POST http://localhost:8080/login \ +curl -i -X POST http://localhost:8080/api/login \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'email=admin@example.com&password=...' \ -c cookies.txt @@ -205,7 +211,7 @@ async fn post_login( #[doc = r#"Login (JSON) and issue session + CSRF cookies. -Accepts: `POST /login.json` (`application/json`) +Accepts: `POST /api/login.json` (`application/json`) - Body: `{ "email": "...", "password": "..." }` - On success: `{"ok": true}` and `Set-Cookie` headers for session + CSRF @@ -218,7 +224,7 @@ Note: Example: ```bash -curl -i -X POST http://localhost:8080/login.json \ +curl -i -X POST http://localhost:8080/api/login.json \ -H 'Content-Type: application/json' \ -d '{"email":"admin@example.com","password":"..."}' \ -c cookies.txt @@ -245,7 +251,7 @@ async fn post_login_json( #[doc = r#"Logout and clear cookies. -Accepts: `POST /logout` +Accepts: `POST /api/logout` Security: - Requires a valid CSRF header (double-submit) via [`CsrfGuard`] @@ -256,7 +262,7 @@ Responses: Example: ```bash -curl -i -X POST http://localhost:8080/logout \ +curl -i -X POST http://localhost:8080/api/logout \ -H "Cookie: __Host-session=...; __Host-csrf=..." \ -H "X-CSRF-Token: " ``` @@ -277,7 +283,7 @@ async fn post_logout(mut jar: PrivateCookieJar, _csrf: CsrfGuard) -> axum::respo #[doc = r#"Create a sleep session. -Accepts: `POST /sleep` (`application/json`) +Accepts: `POST /api/sleep` (`application/json`) - Body: [`SleepInput`] Security: @@ -291,7 +297,7 @@ Responses: Example: ```bash -curl -i -X POST http://localhost:8080/sleep \ +curl -i -X POST http://localhost:8080/api/sleep \ -H "Cookie: __Host-session=...; __Host-csrf=..." \ -H "X-CSRF-Token: " \ -H "Content-Type: application/json" \ @@ -312,7 +318,7 @@ async fn create_sleep( #[doc = r#"Get a sleep session for a wake date. -Accepts: `GET /sleep/date/{date}` +Accepts: `GET /api/sleep/date/{date}` - Path param `date`: `YYYY-MM-DD` (wake date) Security: @@ -338,7 +344,7 @@ async fn get_sleep( #[doc = r#"Update a sleep session by id. -Accepts: `PUT /sleep/{id}` (`application/json`) +Accepts: `PUT /api/sleep/{id}` (`application/json`) - Body: [`SleepInput`] Security: @@ -365,7 +371,7 @@ async fn update_sleep( #[doc = r#"Delete a sleep session by id. -Accepts: `DELETE /sleep/{id}` +Accepts: `DELETE /api/sleep/{id}` Security: - Requires authenticated session ([`RequireSessionJson`]) @@ -439,3 +445,149 @@ async fn create_note( let id = handlers::create_note(&db, input).await?; Ok((StatusCode::CREATED, Json(json!({"id": id})))) } + +#[derive(serde::Deserialize)] +struct RecentParams { + days: Option, +} + +#[derive(serde::Deserialize)] +struct RangeParams { + from: chrono::NaiveDate, + to: chrono::NaiveDate, +} + +#[doc = r#"List recent sleep entries. + +Accepts: `GET /api/sleep/recent?days=7` +- days clamped to [1, 31]; defaults to 7 when missing + +Security: +- Requires authenticated session ([`RequireSessionJson`]) + +Responses: +- 200 OK — `Vec` (ordered desc by date) +- 400 Bad Request — `{code,message}` on invalid params +"#] +async fn get_sleep_recent( + State(db): State, + RequireSessionJson { _user_id: _ }: RequireSessionJson, + axum::extract::Query(params): axum::extract::Query, +) -> impl IntoResponse { + let days = match params.days { + None => 7, + Some(d) if (1..=31).contains(&d) => d, + _ => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"code":"bad_request","message":"days must be between 1 and 31"})), + ) + .into_response(); + } + }; + match crate::repository::list_recent_sleep(&db, days).await { + Ok(items) => Json(items).into_response(), + Err(e) => ApiError::Db(e).into_response(), + } +} + +#[doc = r#"List sleep entries in an inclusive date range. + +Accepts: `GET /api/sleep/range?from=YYYY-MM-DD&to=YYYY-MM-DD` +- Validates `from <= to` +- Range length must be ≤ 62 days + +Security: +- Requires authenticated session ([`RequireSessionJson`]) + +Responses: +- 200 OK — `Vec` (ordered asc by date) +- 400 Bad Request — `{code,message}` on invalid params +"#] +async fn get_sleep_range( + State(db): State, + RequireSessionJson { _user_id: _ }: RequireSessionJson, + axum::extract::Query(params): axum::extract::Query, +) -> impl IntoResponse { + if params.from > params.to { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"code":"bad_request","message":"from must be <= to"})), + ) + .into_response(); + } + let span_days = (params.to - params.from).num_days() + 1; + if span_days > 62 { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"code":"bad_request","message":"range must be <= 62 days"})), + ) + .into_response(); + } + match crate::repository::list_sleep_range(&db, params.from, params.to).await { + Ok(items) => Json(items).into_response(), + Err(e) => ApiError::Db(e).into_response(), + } +} + +#[doc = r#"Get a sleep session by id. + +Accepts: `GET /api/sleep/{id}` + +Security: +- Requires authenticated session ([`RequireSessionJson`]) + +Responses: +- 200 OK — [`SleepSession`] +- 401 Unauthorized — no/invalid session +- 404 Not Found — no entry for id +"#] +async fn get_sleep_by_id( + State(db): State, + RequireSessionJson { _user_id: _ }: RequireSessionJson, + Path(id): Path, +) -> Result { + match crate::repository::find_sleep_by_id(&db, id).await? { + Some(s) => Ok(Json(s)), + None => Err(ApiError::NotFound), + } +} + +#[doc = r#"List exercise intensity for a date range. + +Accepts: `GET /api/exercise/intensity?from=YYYY-MM-DD&to=YYYY-MM-DD` +- Validates `from <= to` +- Range length must be ≤ 62 days + +Security: +- Requires authenticated session ([`RequireSessionJson`]) + +Responses: +- 200 OK — `Vec<{date, intensity}>` ordered asc by date +- 400 Bad Request — `{code,message}` on invalid params +"#] +async fn get_exercise_intensity( + State(db): State, + RequireSessionJson { _user_id: _ }: RequireSessionJson, + axum::extract::Query(params): axum::extract::Query, +) -> impl IntoResponse { + if params.from > params.to { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"code":"bad_request","message":"from must be <= to"})), + ) + .into_response(); + } + let span_days = (params.to - params.from).num_days() + 1; + if span_days > 62 { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"code":"bad_request","message":"range must be <= 62 days"})), + ) + .into_response(); + } + match crate::repository::list_exercise_intensity(&db, params.from, params.to).await { + Ok(items) => Json(items).into_response(), + Err(e) => ApiError::Db(e).into_response(), + } +} diff --git a/sleep-api/src/error.rs b/sleep-api/src/error.rs index 2598e45..01230b3 100644 --- a/sleep-api/src/error.rs +++ b/sleep-api/src/error.rs @@ -25,16 +25,18 @@ impl IntoResponse for ApiError { error!(?e, "database error"); ( StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({"error":"database error","detail": e.to_string()})), + Json(json!({"code":"internal","message":"database error","detail": e.to_string()})), ) .into_response() } - ApiError::NotFound => { - (StatusCode::NOT_FOUND, Json(json!({"error":"not found"}))).into_response() - } + ApiError::NotFound => ( + StatusCode::NOT_FOUND, + Json(json!({"code":"not_found","message":"not found"})), + ) + .into_response(), ApiError::InvalidInput(msg) => ( StatusCode::BAD_REQUEST, - Json(json!({"error":"invalid input","detail": msg})), + Json(json!({"code":"bad_request","message": msg})), ) .into_response(), } diff --git a/sleep-api/src/models/exercise.rs b/sleep-api/src/models/exercise.rs index 6a30a57..5714353 100644 --- a/sleep-api/src/models/exercise.rs +++ b/sleep-api/src/models/exercise.rs @@ -2,6 +2,7 @@ use super::intensity::Intensity; use crate::domain::DomainError; use chrono::{NaiveDate, NaiveTime}; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; #[doc = r#"User-provided input representing an exercise event. @@ -38,6 +39,12 @@ pub struct ExerciseInput { pub duration_min: Option, } +#[derive(Serialize, Deserialize, Debug, PartialEq, FromRow, Clone)] +pub struct DateIntensity { + pub date: NaiveDate, + pub intensity: String, // "none" | "light" | "hard" +} + impl ExerciseInput { #[doc = r#"Validate the exercise input. diff --git a/sleep-api/src/models/mod.rs b/sleep-api/src/models/mod.rs index 85fb811..7a2cb36 100644 --- a/sleep-api/src/models/mod.rs +++ b/sleep-api/src/models/mod.rs @@ -15,10 +15,10 @@ pub mod note; pub mod quality; pub mod sleep; -pub use exercise::ExerciseInput; +pub use exercise::{DateIntensity, ExerciseInput}; #[allow(unused_imports)] pub use intensity::Intensity; pub use note::NoteInput; #[allow(unused_imports)] pub use quality::Quality; -pub use sleep::{SleepInput, SleepSession}; +pub use sleep::{SleepInput, SleepListItem, SleepSession}; diff --git a/sleep-api/src/models/sleep.rs b/sleep-api/src/models/sleep.rs index 62c3b6e..cbf2bd2 100644 --- a/sleep-api/src/models/sleep.rs +++ b/sleep-api/src/models/sleep.rs @@ -97,3 +97,31 @@ pub struct SleepSession { pub awakenings: i32, pub quality: i32, } + +#[doc = r#"List item projection for v_daily_sleep. + +Used by GET /api/sleep/recent and GET /api/sleep/range. Note that the SQL maps +`wake_date` to `date` via `AS date` to align with the existing field name. +`duration_min` is nullable (computed on insert/update; may be NULL for legacy rows). + +Fields mirror v_daily_sleep columns: +- id +- date (wake date) +- bed_time +- wake_time +- latency_min +- awakenings +- quality +- duration_min (nullable) +"#] +#[derive(Serialize, Deserialize, Debug, PartialEq, FromRow, Clone)] +pub struct SleepListItem { + pub id: i64, + pub date: NaiveDate, + pub bed_time: NaiveTime, + pub wake_time: NaiveTime, + pub latency_min: i32, + pub awakenings: i32, + pub quality: i32, + pub duration_min: Option, +} diff --git a/sleep-api/src/repository.rs b/sleep-api/src/repository.rs index 43d8305..c3da798 100644 --- a/sleep-api/src/repository.rs +++ b/sleep-api/src/repository.rs @@ -17,7 +17,7 @@ See also: use crate::{ db::Db, - models::{ExerciseInput, NoteInput, SleepInput, SleepSession}, + models::{DateIntensity, ExerciseInput, NoteInput, SleepInput, SleepListItem, SleepSession}, }; use chrono::NaiveDate; use sqlx::{Sqlite, Transaction}; @@ -109,6 +109,25 @@ pub async fn find_sleep_by_date( .await } +#[doc = r#"Find a sleep session by id. + +Returns `Ok(None)` if no session exists for the provided id. + +See the example on [`insert_sleep`]. + +# Errors +- Returns [`sqlx::Error`] on database errors. +"#] +pub async fn find_sleep_by_id(db: &Db, id: i64) -> Result, sqlx::Error> { + sqlx::query_as::( + r#"SELECT s.id, s.date, s.bed_time, s.wake_time, m.latency_min, m.awakenings, m.quality + FROM sleep_sessions s JOIN sleep_metrics m ON m.session_id = s.id WHERE s.id = ?"#, + ) + .bind(id) + .fetch_optional(db) + .await +} + #[doc = r#"Update a sleep session and its metrics in a single transaction. Requires a recomputed `duration_min`; see [`time::compute_duration_min`]. @@ -162,6 +181,88 @@ pub async fn delete_sleep(db: &Db, id: i64) -> Result { Ok(res.rows_affected()) } +#[doc = r#"List last N daily sleep entries ordered by date DESC. + +Backed by the v_daily_sleep view. Maps wake_date -> date via SQL alias to match API struct."#] +pub async fn list_recent_sleep(db: &Db, days: i32) -> Result, sqlx::Error> { + sqlx::query_as::( + r#"SELECT id, + wake_date AS date, + bed_time, + wake_time, + latency_min, + awakenings, + quality, + duration_min + FROM v_daily_sleep + ORDER BY date DESC + LIMIT ?"#, + ) + .bind(days) + .fetch_all(db) + .await +} + +#[doc = r#"List exercise intensity by date in the inclusive range [from, to]. + +For each date, returns the highest intensity among any events on that date. + +- "none" < "light" < "hard" + +Ordered by date ASC. +"#] +pub async fn list_exercise_intensity( + db: &Db, + from: NaiveDate, + to: NaiveDate, +) -> Result, sqlx::Error> { + // Map intensity to ordinal to pick max, then map back to string + sqlx::query_as::( + r#" + SELECT + date, + CASE MAX(CASE intensity WHEN 'none' THEN 0 WHEN 'light' THEN 1 WHEN 'hard' THEN 2 ELSE 0 END) + WHEN 2 THEN 'hard' + WHEN 1 THEN 'light' + ELSE 'none' + END AS intensity + FROM exercise_events + WHERE date BETWEEN ? AND ? + GROUP BY date + ORDER BY date ASC + "#, + ) + .bind(from) + .bind(to) + .fetch_all(db) + .await +} + +#[doc = r#"List daily sleep entries in the inclusive range [from, to] ordered by date ASC."#] +pub async fn list_sleep_range( + db: &Db, + from: NaiveDate, + to: NaiveDate, +) -> Result, sqlx::Error> { + sqlx::query_as::( + r#"SELECT id, + wake_date AS date, + bed_time, + wake_time, + latency_min, + awakenings, + quality, + duration_min + FROM v_daily_sleep + WHERE wake_date BETWEEN ? AND ? + ORDER BY date ASC"#, + ) + .bind(from) + .bind(to) + .fetch_all(db) + .await +} + #[doc = r#"Insert an exercise event. # Example (minimal) @@ -191,9 +292,42 @@ let id = repository::insert_exercise(&db, &input).await?; - Returns [`sqlx::Error`] on database errors. "#] pub async fn insert_exercise(db: &Db, input: &ExerciseInput) -> Result { + // For "daily intensity" sentinel rows (no time and no duration), upsert by date + if input.start_time.is_none() && input.duration_min.is_none() { + let mut tx: Transaction<'_, Sqlite> = db.begin().await?; + if let Some(existing_id) = sqlx::query_scalar::( + "SELECT id FROM exercise_events WHERE date = ? AND start_time IS NULL AND duration_min IS NULL", + ) + .bind(input.date) + .fetch_optional(&mut *tx) + .await? + { + sqlx::query::("UPDATE exercise_events SET intensity = ? WHERE id = ?") + .bind(input.intensity.to_string()) + .bind(existing_id) + .execute(&mut *tx) + .await?; + tx.commit().await?; + return Ok(existing_id); + } else { + let res = sqlx::query::( + "INSERT INTO exercise_events(date, intensity, start_time, duration_min) VALUES (?, ?, ?, ?)", + ) + .bind(input.date) + .bind(input.intensity.to_string()) + .bind(None::) + .bind(None::) + .execute(&mut *tx) + .await?; + tx.commit().await?; + return Ok(res.last_insert_rowid()); + } + } + + // Otherwise, treat as a normal exercise event insert let mut tx: Transaction<'_, Sqlite> = db.begin().await?; let res = sqlx::query::( - "INSERT INTO exercise_events(date, intensity, start_time, duration_min) VALUES (?, ?, ?, ?)" + "INSERT INTO exercise_events(date, intensity, start_time, duration_min) VALUES (?, ?, ?, ?)", ) .bind(input.date) .bind(input.intensity.to_string()) diff --git a/sleep-api/tests/api_exercise_intensity.rs b/sleep-api/tests/api_exercise_intensity.rs new file mode 100644 index 0000000..a38180c --- /dev/null +++ b/sleep-api/tests/api_exercise_intensity.rs @@ -0,0 +1,182 @@ +use argon2::password_hash::rand_core::OsRng; +use argon2::{ + Argon2, + password_hash::{PasswordHasher, SaltString}, +}; +use reqwest::Client; +use sleep_api::{app, db}; + +fn set_admin_env(email: &str, password: &str) { + let salt = SaltString::generate(OsRng); + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(password.as_bytes(), &salt) + .unwrap() + .to_string(); + unsafe { + std::env::set_var("ADMIN_EMAIL", email); + std::env::set_var("ADMIN_PASSWORD_HASH", hash); + } +} + +async fn wait_ready(client: &Client, addr: &str) { + let health_url = format!("http://{addr}/api/health"); + for _ in 0..20 { + if client.get(&health_url).send().await.is_ok() { + return; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + panic!("Server did not become ready in time"); +} + +fn parse_cookie<'a>( + headers: impl Iterator, + name_with_eq: &str, +) -> Option { + for hv in headers { + if let Ok(s) = hv.to_str() { + if s.starts_with(name_with_eq) { + if let Some(eq_idx) = s.find('=') { + let rest = &s[eq_idx + 1..]; + let end = rest.find(';').unwrap_or(rest.len()); + return Some(rest[..end].to_string()); + } + } + } + } + None +} + +async fn login_and_get_auth( + client: &Client, + addr: &str, + email: &str, + password: &str, +) -> (String, String) { + let res = client + .post(format!("http://{addr}/api/login.json")) + .json(&serde_json::json!({ "email": email, "password": password })) + .send() + .await + .expect("login request failed"); + assert_eq!(res.status(), 200, "login failed: {}", res.status()); + let headers = res.headers().get_all(reqwest::header::SET_COOKIE); + // Accept both secure (__Host-*) and dev-mode (no prefix) cookie names + let csrf = parse_cookie(headers.iter(), "__Host-csrf=") + .or_else(|| parse_cookie(headers.iter(), "csrf=")) + .expect("missing CSRF cookie in login response"); + let session = parse_cookie(headers.iter(), "__Host-session=") + .or_else(|| parse_cookie(headers.iter(), "session=")) + .expect("missing session cookie in login response"); + (csrf, session) +} + +#[derive(serde::Deserialize, Debug)] +struct DateIntensity { + date: chrono::NaiveDate, + intensity: String, // "none" | "light" | "hard" +} + +#[tokio::test] +async fn test_exercise_intensity_range_endpoint() { + unsafe { + std::env::set_var("DATABASE_URL", "sqlite::memory:"); + std::env::set_var("COOKIE_SECURE", "0"); + }; + set_admin_env("admin@example.com", "password123"); + + let pool = db::connect().await.unwrap(); + sqlx::migrate::Migrator::new(std::path::Path::new("../migrations")) + .await + .unwrap() + .run(&pool) + .await + .unwrap(); + + let app = app::router(pool.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.2:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let client = Client::builder().cookie_store(true).build().unwrap(); + wait_ready(&client, &addr.to_string()).await; + + let (csrf, session_cookie) = login_and_get_auth( + &client, + &addr.to_string(), + "admin@example.com", + "password123", + ) + .await; + + // Seed exercise events: + // 2025-06-10: light + // 2025-06-11: none + // 2025-06-12: light then hard (final should be "hard") + let seeds = vec![ + ("2025-06-10", "light"), + ("2025-06-11", "none"), + ("2025-06-12", "light"), + ("2025-06-12", "hard"), + ]; + for (date, intensity) in seeds { + let res = client + .post(format!("http://{addr}/api/exercise")) + .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) + .header("X-CSRF-Token", &csrf) + .json(&serde_json::json!({ + "date": date, + "intensity": intensity + })) + .send() + .await + .unwrap(); + let status = res.status(); + if status != 201 { + let body = res.text().await.unwrap_or_else(|_| "".into()); + panic!("seed exercise failed: {} body: {}", status, body); + } + } + + // Query intensities in [2025-06-10, 2025-06-12] + let res = client + .get(format!( + "http://{addr}/api/exercise/intensity?from=2025-06-10&to=2025-06-12" + )) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200, "intensity status {}", res.status()); + let items: Vec = res.json().await.unwrap(); + assert_eq!(items.len(), 3, "expected 3 days"); + assert_eq!( + items[0].date, + chrono::NaiveDate::from_ymd_opt(2025, 6, 10).unwrap() + ); + assert_eq!(items[0].intensity, "light"); + assert_eq!( + items[1].date, + chrono::NaiveDate::from_ymd_opt(2025, 6, 11).unwrap() + ); + assert_eq!(items[1].intensity, "none"); + assert_eq!( + items[2].date, + chrono::NaiveDate::from_ymd_opt(2025, 6, 12).unwrap() + ); + assert_eq!(items[2].intensity, "hard"); + + // Invalid range: from > to => 400 + let res = client + .get(format!( + "http://{addr}/api/exercise/intensity?from=2025-06-13&to=2025-06-12" + )) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + server.abort(); +} diff --git a/sleep-api/tests/api_sleep.rs b/sleep-api/tests/api_sleep.rs index 052c016..47e71ec 100644 --- a/sleep-api/tests/api_sleep.rs +++ b/sleep-api/tests/api_sleep.rs @@ -22,7 +22,7 @@ fn set_admin_env(email: &str, password: &str) { } async fn wait_ready(client: &Client, addr: &str) { - let health_url = format!("http://{addr}/health"); + let health_url = format!("http://{addr}/api/health"); for _ in 0..20 { if client.get(&health_url).send().await.is_ok() { return; @@ -57,7 +57,7 @@ async fn login_and_get_auth( password: &str, ) -> (String, String) { let res = client - .post(format!("http://{addr}/login.json")) + .post(format!("http://{addr}/api/login.json")) .json(&serde_json::json!({ "email": email, "password": password })) .send() .await @@ -117,7 +117,7 @@ async fn test_sleep_flow() { quality: Quality(4), }; let res = client - .post(format!("http://{addr}/sleep")) + .post(format!("http://{addr}/api/sleep")) .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .json(&input) @@ -128,8 +128,18 @@ async fn test_sleep_flow() { let id: serde_json::Value = res.json().await.unwrap(); let id = id["id"].as_i64().unwrap(); + // Fetch by id and verify let res = client - .get(format!("http://{addr}/sleep/date/{}", input.date)) + .get(format!("http://{addr}/api/sleep/{id}")) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + let session_by_id: SleepSession = res.json().await.unwrap(); + assert_eq!(session_by_id.id, id); + + let res = client + .get(format!("http://{addr}/api/sleep/date/{}", input.date)) .send() .await .unwrap(); @@ -145,7 +155,7 @@ async fn test_sleep_flow() { ..input.clone() }; let res = client - .put(format!("http://{addr}/sleep/{id}")) + .put(format!("http://{addr}/api/sleep/{id}")) .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .json(&updated) @@ -155,7 +165,7 @@ async fn test_sleep_flow() { assert_eq!(res.status(), 204); let res = client - .get(format!("http://{addr}/sleep/date/{}", updated.date)) + .get(format!("http://{addr}/api/sleep/date/{}", updated.date)) .send() .await .unwrap(); @@ -165,7 +175,7 @@ async fn test_sleep_flow() { assert_eq!(session.latency_min, updated.latency_min); let res = client - .delete(format!("http://{addr}/sleep/{id}")) + .delete(format!("http://{addr}/api/sleep/{id}")) .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .send() @@ -175,7 +185,7 @@ async fn test_sleep_flow() { // Idempotency: deleting the same id again should still return 204 let res = client - .delete(format!("http://{addr}/sleep/{id}")) + .delete(format!("http://{addr}/api/sleep/{id}")) .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .send() @@ -184,7 +194,7 @@ async fn test_sleep_flow() { assert_eq!(res.status(), 204, "idempotent delete should be 204"); let res = client - .get(format!("http://{addr}/sleep/date/{}", updated.date)) + .get(format!("http://{addr}/api/sleep/date/{}", updated.date)) .send() .await .unwrap(); @@ -234,7 +244,7 @@ async fn test_exercise_and_note() { duration_min: Some(30), }; let res = client - .post(format!("http://{addr}/exercise")) + .post(format!("http://{addr}/api/exercise")) .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .json(&exercise) @@ -260,7 +270,7 @@ async fn test_exercise_and_note() { body: Some("Great workout".to_string()), }; let res = client - .post(format!("http://{addr}/note")) + .post(format!("http://{addr}/api/note")) .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .json(¬e) diff --git a/sleep-api/tests/api_sleep_list.rs b/sleep-api/tests/api_sleep_list.rs new file mode 100644 index 0000000..52be5e9 --- /dev/null +++ b/sleep-api/tests/api_sleep_list.rs @@ -0,0 +1,263 @@ +use argon2::password_hash::rand_core::OsRng; +use argon2::{ + Argon2, + password_hash::{PasswordHasher, SaltString}, +}; +use reqwest::Client; +use sleep_api::models::{Quality, SleepInput, SleepListItem}; +use sleep_api::{app, db}; + +fn set_admin_env(email: &str, password: &str) { + let salt = SaltString::generate(OsRng); + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(password.as_bytes(), &salt) + .unwrap() + .to_string(); + unsafe { + std::env::set_var("ADMIN_EMAIL", email); + std::env::set_var("ADMIN_PASSWORD_HASH", hash); + } +} + +async fn wait_ready(client: &Client, addr: &str) { + let health_url = format!("http://{addr}/api/health"); + for _ in 0..20 { + if client.get(&health_url).send().await.is_ok() { + return; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + panic!("Server did not become ready in time"); +} + +fn parse_cookie<'a>( + headers: impl Iterator, + name_with_eq: &str, +) -> Option { + for hv in headers { + if let Ok(s) = hv.to_str() { + if s.starts_with(name_with_eq) { + if let Some(eq_idx) = s.find('=') { + let rest = &s[eq_idx + 1..]; + let end = rest.find(';').unwrap_or(rest.len()); + return Some(rest[..end].to_string()); + } + } + } + } + None +} + +async fn login_and_get_auth( + client: &Client, + addr: &str, + email: &str, + password: &str, +) -> (String, String) { + let res = client + .post(format!("http://{addr}/api/login.json")) + .json(&serde_json::json!({ "email": email, "password": password })) + .send() + .await + .expect("login request failed"); + assert_eq!(res.status(), 200, "login failed: {}", res.status()); + let headers = res.headers().get_all(reqwest::header::SET_COOKIE); + // Accept both secure (__Host-*) and dev-mode (no prefix) cookie names + let csrf = parse_cookie(headers.iter(), "__Host-csrf=") + .or_else(|| parse_cookie(headers.iter(), "csrf=")) + .expect("missing CSRF cookie in login response"); + let session = parse_cookie(headers.iter(), "__Host-session=") + .or_else(|| parse_cookie(headers.iter(), "session=")) + .expect("missing session cookie in login response"); + (csrf, session) +} + +async fn seed_sleep( + client: &Client, + addr: &str, + csrf: &str, + session_cookie: &str, + date: (i32, u32, u32), + bed: (u32, u32, u32), + wake: (u32, u32, u32), + quality: i32, +) { + let input = SleepInput { + date: chrono::NaiveDate::from_ymd_opt(date.0, date.1, date.2).unwrap(), + bed_time: chrono::NaiveTime::from_hms_opt(bed.0, bed.1, bed.2).unwrap(), + wake_time: chrono::NaiveTime::from_hms_opt(wake.0, wake.1, wake.2).unwrap(), + latency_min: 10, + awakenings: 1, + quality: Quality(quality as u8), + }; + let res = client + .post(format!("http://{addr}/api/sleep")) + .header("Cookie", format!("session={session_cookie}; csrf={csrf}")) + .header("X-CSRF-Token", csrf) + .json(&input) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 201, "seed sleep failed: {}", res.status()); +} + +#[tokio::test] +async fn test_sleep_list_recent_and_range() { + unsafe { + std::env::set_var("DATABASE_URL", "sqlite::memory:"); + std::env::set_var("COOKIE_SECURE", "0"); + }; + set_admin_env("admin@example.com", "password123"); + + let pool = db::connect().await.unwrap(); + sqlx::migrate::Migrator::new(std::path::Path::new("../migrations")) + .await + .unwrap() + .run(&pool) + .await + .unwrap(); + + let app = app::router(pool.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.2:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let client = Client::builder().cookie_store(true).build().unwrap(); + wait_ready(&client, &addr.to_string()).await; + + // Login + let (csrf, session_cookie) = login_and_get_auth( + &client, + &addr.to_string(), + "admin@example.com", + "password123", + ) + .await; + + // Seed 9 days of entries (2025-06-10 .. 2025-06-18) + for d in 10..=18 { + seed_sleep( + &client, + &addr.to_string(), + &csrf, + &session_cookie, + (2025, 6, d), + (22, 0, 0), + (6, 0, 0), + if d % 2 == 0 { 4 } else { 3 }, + ) + .await; + } + + // GET /sleep/recent?days=7 -> <= 7 items, desc by date + let res = client + .get(format!("http://{addr}/api/sleep/recent?days=7")) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200, "recent status {}", res.status()); + let recent: Vec = res.json().await.unwrap(); + assert!(recent.len() <= 7, "recent length {}", recent.len()); + let mut prev = recent.first().unwrap().date; + for item in recent.iter().skip(1) { + assert!(item.date <= prev, "not desc: {} then {}", prev, item.date); + prev = item.date; + } + + // GET /sleep/range?from=2025-06-12&to=2025-06-15 -> 4 items, asc by date + let res = client + .get(format!( + "http://{addr}/api/sleep/range?from=2025-06-12&to=2025-06-15" + )) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200, "range status {}", res.status()); + let range: Vec = res.json().await.unwrap(); + assert_eq!(range.len(), 4, "range length {}", range.len()); + let mut prev_a = range.first().unwrap().date; + for item in range.iter().skip(1) { + assert!( + item.date >= prev_a, + "not asc: {} then {}", + prev_a, + item.date + ); + prev_a = item.date; + } + + server.abort(); +} + +#[tokio::test] +async fn test_sleep_list_invalid_params() { + unsafe { + std::env::set_var("DATABASE_URL", "sqlite::memory:"); + std::env::set_var("COOKIE_SECURE", "0"); + }; + set_admin_env("admin@example.com", "password123"); + + let pool = db::connect().await.unwrap(); + sqlx::migrate::Migrator::new(std::path::Path::new("../migrations")) + .await + .unwrap() + .run(&pool) + .await + .unwrap(); + + let app = app::router(pool.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.2:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let client = Client::builder().cookie_store(true).build().unwrap(); + wait_ready(&client, &addr.to_string()).await; + + // Login + let (_csrf, _session_cookie) = login_and_get_auth( + &client, + &addr.to_string(), + "admin@example.com", + "password123", + ) + .await; + + // days=0 -> 400 + let res = client + .get(format!("http://{addr}/api/sleep/recent?days=0")) + .send() + .await + .unwrap(); + assert_eq!( + res.status(), + 400, + "days=0 should be 400 per validation policy" + ); + + // from > to -> 400 + let res = client + .get(format!( + "http://{addr}/api/sleep/range?from=2025-07-02&to=2025-07-01" + )) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + // range > 62 days -> 400 + let res = client + .get(format!( + "http://{addr}/api/sleep/range?from=2025-01-01&to=2025-03-15" + )) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + server.abort(); +} diff --git a/sleep-api/tests/auth_csrf.rs b/sleep-api/tests/auth_csrf.rs index deb763a..91e68c3 100644 --- a/sleep-api/tests/auth_csrf.rs +++ b/sleep-api/tests/auth_csrf.rs @@ -24,7 +24,7 @@ fn set_admin_env(email: &str, password: &str) { } async fn wait_ready(client: &Client, addr: &str) { - let health_url = format!("http://{addr}/health"); + let health_url = format!("http://{addr}/api/health"); for _ in 0..20 { if client.get(&health_url).send().await.is_ok() { return; @@ -91,7 +91,7 @@ async fn test_auth_and_csrf_flow() { // HEAD /health should be OK let res = client - .head(format!("http://{addr}/health")) + .head(format!("http://{addr}/api/health")) .send() .await .unwrap(); @@ -116,7 +116,7 @@ async fn test_auth_and_csrf_flow() { "password": "password123" }); let login_res = client - .post(format!("http://{addr}/login.json")) + .post(format!("http://{addr}/api/login.json")) .json(&login_body) .send() .await @@ -161,7 +161,7 @@ async fn test_auth_and_csrf_flow() { quality: Quality(4), }; let res = client - .post(format!("http://{addr}/sleep")) + .post(format!("http://{addr}/api/sleep")) .json(&sample) .send() .await @@ -174,7 +174,7 @@ async fn test_auth_and_csrf_flow() { // Mutating API with CSRF header should succeed let res = client - .post(format!("http://{addr}/sleep")) + .post(format!("http://{addr}/api/sleep")) .header( "Cookie", format!("__Host-session={session}; __Host-csrf={csrf}"), @@ -194,7 +194,7 @@ async fn test_auth_and_csrf_flow() { // Logout clears session; subsequent mutating should be 401 let res = client - .post(format!("http://{addr}/logout")) + .post(format!("http://{addr}/api/logout")) .header( "Cookie", format!("__Host-session={session}; __Host-csrf={csrf}"), @@ -216,7 +216,7 @@ async fn test_auth_and_csrf_flow() { assert_eq!(v3["authenticated"], false); let res = client - .delete(format!("http://{addr}/sleep/{id}")) + .delete(format!("http://{addr}/api/sleep/{id}")) .header("X-CSRF-Token", &csrf) .send() .await @@ -269,7 +269,7 @@ async fn test_csrf_percent_encoded_header() { "password": "password123" }); let res = client - .post(format!("http://{addr}/login.json")) + .post(format!("http://{addr}/api/login.json")) .json(&login_body) .send() .await @@ -300,7 +300,7 @@ async fn test_csrf_percent_encoded_header() { quality: Quality(4), }; let res = client - .post(format!("http://{addr}/sleep")) + .post(format!("http://{addr}/api/sleep")) .header( "Cookie", format!("__Host-session={session}; __Host-csrf={csrf}"), @@ -347,7 +347,7 @@ async fn test_dev_cookie_names_and_flags() { wait_ready(&client, &addr.to_string()).await; let res = client - .post(format!("http://{addr}/login.json")) + .post(format!("http://{addr}/api/login.json")) .json(&serde_json::json!({ "email":"admin@example.com", "password":"password123" })) .send() .await @@ -399,7 +399,7 @@ async fn test_dev_cookie_names_and_flags() { quality: Quality(5), }; let res = client - .post(format!("http://{addr}/sleep")) + .post(format!("http://{addr}/api/sleep")) .header("Cookie", format!("session={session_val}; csrf={csrf_val}")) .header("X-CSRF-Token", &csrf_val) .json(&sample) diff --git a/sleep-api/tests/trends_bars.rs b/sleep-api/tests/trends_bars.rs index 83066bc..a4ff42c 100644 --- a/sleep-api/tests/trends_bars.rs +++ b/sleep-api/tests/trends_bars.rs @@ -21,7 +21,7 @@ fn set_admin_env(email: &str, password: &str) { } async fn wait_ready(client: &Client, addr: &str) { - let health_url = format!("http://{addr}/health"); + let health_url = format!("http://{addr}/api/health"); for _ in 0..20 { if client.get(&health_url).send().await.is_ok() { return; @@ -56,7 +56,7 @@ async fn login_and_get_auth( password: &str, ) -> (String, String) { let res = client - .post(format!("http://{addr}/login.json")) + .post(format!("http://{addr}/api/login.json")) .json(&serde_json::json!({ "email": email, "password": password })) .send() .await @@ -131,7 +131,7 @@ async fn test_trends_sleep_bars_basic() { }; let res = client - .post(format!("http://{addr}/sleep")) + .post(format!("http://{addr}/api/sleep")) .header("Cookie", format!("session={session}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .json(&s1) @@ -141,7 +141,7 @@ async fn test_trends_sleep_bars_basic() { assert_eq!(res.status(), 201); let res = client - .post(format!("http://{addr}/sleep")) + .post(format!("http://{addr}/api/sleep")) .header("Cookie", format!("session={session}; csrf={csrf}")) .header("X-CSRF-Token", &csrf) .json(&s2) diff --git a/sleep-ui/package-lock.json b/sleep-ui/package-lock.json index 5779070..55c0ecc 100644 --- a/sleep-ui/package-lock.json +++ b/sleep-ui/package-lock.json @@ -18,12 +18,143 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/vite": "^4.1.12", "@types/node": "^24.2.1", + "jsdom": "^26.1.0", "svelte": "^5.38.1", "svelte-check": "^4.3.1", "tailwindcss": "^4.1.11", "tslib": "^2.6.3", "typescript": "^5.5.4", - "vite": "^7.1.2" + "vite": "^7.1.2", + "vitest": "^1.6.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/aix-ppc64": { @@ -451,6 +582,23 @@ "node": ">=18" } }, + "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/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -464,6 +612,19 @@ "node": ">=18.0.0" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -822,6 +983,13 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -1303,6 +1471,80 @@ "undici-types": "~7.10.0" } }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1316,6 +1558,42 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -1326,6 +1604,16 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -1345,6 +1633,35 @@ "optional": true, "peer": true }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chart.js": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", @@ -1357,6 +1674,19 @@ "pnpm": ">=8" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1393,6 +1723,13 @@ "node": ">=6" } }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -1403,6 +1740,49 @@ "node": ">= 0.6" } }, + "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/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -1421,6 +1801,26 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1448,6 +1848,16 @@ "dev": true, "license": "MIT" }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -1462,6 +1872,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -1538,6 +1961,40 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -1568,13 +2025,107 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -1585,6 +2136,26 @@ "@types/estree": "^1.0.6" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -1595,6 +2166,53 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1844,6 +2462,23 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -1851,6 +2486,23 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -1861,6 +2513,26 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -1910,6 +2582,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -1956,6 +2648,114 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1976,6 +2776,25 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/playwright": { "version": "1.54.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", @@ -2037,6 +2856,38 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2105,6 +2956,13 @@ "win32" ] }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -2118,6 +2976,26 @@ "node": ">=6" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", @@ -2125,6 +3003,49 @@ "dev": true, "license": "MIT" }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", @@ -2175,17 +3096,57 @@ "node": ">=0.10.0" } }, - "node_modules/svelte": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.1.tgz", - "integrity": "sha512-fO6CLDfJYWHgfo6lQwkQU2vhCiHc2MBl6s3vEhK+sSZru17YL4R5s1v14ndRpqKAIkq8nCz6MTk1yZbESZWeyQ==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", - "@types/estree": "^1.0.5", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/svelte": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.1.tgz", + "integrity": "sha512-fO6CLDfJYWHgfo6lQwkQU2vhCiHc2MBl6s3vEhK+sSZru17YL4R5s1v14ndRpqKAIkq8nCz6MTk1yZbESZWeyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", @@ -2225,6 +3186,13 @@ "typescript": ">=5.0.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", @@ -2300,6 +3268,13 @@ "optional": true, "peer": true }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -2317,6 +3292,46 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -2327,6 +3342,32 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2334,6 +3375,16 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/typescript": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", @@ -2348,6 +3399,13 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -2430,54 +3488,1264 @@ } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=12" } }, - "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "node_modules/vite-node/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", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" + "optional": true, + "os": [ + "darwin" ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } + "engines": { + "node": ">=12" } }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "node_modules/vite-node/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": "ISC", + "license": "MIT", "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 14.6" + "node": ">=12" + } + }, + "node_modules/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/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/vite-node/node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "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/vite/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/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/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/vitest/node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "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/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/zimmerframe": { diff --git a/sleep-ui/package.json b/sleep-ui/package.json index b5ae65a..77d858f 100644 --- a/sleep-ui/package.json +++ b/sleep-ui/package.json @@ -9,6 +9,7 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "test:e2e": "playwright test", + "test:unit": "vitest run", "sync": "svelte-kit sync" }, "devDependencies": { @@ -19,12 +20,14 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/vite": "^4.1.12", "@types/node": "^24.2.1", + "jsdom": "^26.1.0", "svelte": "^5.38.1", "svelte-check": "^4.3.1", "tailwindcss": "^4.1.11", "tslib": "^2.6.3", "typescript": "^5.5.4", - "vite": "^7.1.2" + "vite": "^7.1.2", + "vitest": "^1.6.0" }, "dependencies": { "chart.js": "^4.4.3" diff --git a/sleep-ui/src/lib/api.ts b/sleep-ui/src/lib/api.ts index a386db9..0f1b7c7 100644 --- a/sleep-ui/src/lib/api.ts +++ b/sleep-ui/src/lib/api.ts @@ -4,7 +4,7 @@ * - Attach X-CSRF-Token for mutating requests by mirroring CSRF cookie */ -type Json = Record | unknown[]; +export type Json = Record | unknown[]; function isBrowser(): boolean { return typeof window !== 'undefined' && typeof document !== 'undefined'; @@ -120,3 +120,91 @@ export async function apiDelete(path: string, init: RequestInit = {}): Promise { + const method = (init.method ?? 'GET').toString().toUpperCase(); + const isMutating = method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH'; + const csrf = isMutating ? readCsrfToken() : null; + const headers = isMutating && csrf + ? mergeHeaders({ 'X-CSRF-Token': csrf }, init.headers) + : mergeHeaders(init.headers); + return fetch(path, { credentials: 'include', ...init, headers }); +} + +// ------------------------------ +// Types matching OpenAPI schemas +// ------------------------------ +export type IsoDate = string; // YYYY-MM-DD +export type IsoTime = string; // HH:mm:ss + +export interface SleepListItem { + id: number; + date: IsoDate; + bed_time: IsoTime; + wake_time: IsoTime; + latency_min: number; + awakenings: number; + quality: number; + duration_min: number | null; +} + +export interface SleepInput { + date: IsoDate; + bed_time: IsoTime; + wake_time: IsoTime; + latency_min: number; + awakenings: number; + quality: number; +} + +export interface SleepSession extends SleepInput { + id: number; +} + +export interface ExerciseUpsert { + date: IsoDate; + intensity: 'none' | 'light' | 'hard'; +} + +// ------------------------------ +// Helper APIs for Sleep/Exercise +// ------------------------------ +export async function getRecent(days = 7): Promise { + return apiGet(`/api/sleep/recent?days=${days}`); +} + +export async function getRange(from: IsoDate, to: IsoDate): Promise { + return apiGet(`/api/sleep/range?from=${from}&to=${to}`); +} + +export async function getSleepById(id: number): Promise { + return apiGet(`/api/sleep/${id}`); +} + +export async function createSleep(input: SleepInput): Promise<{ id: number }> { + return apiPost<{ id: number }>('/api/sleep', input as unknown as Json); +} + +export async function updateSleep(id: number, input: SleepInput): Promise { + await apiPut(`/api/sleep/${id}`, input as unknown as Json); +} + +export async function deleteSleep(id: number): Promise { + await apiDelete(`/api/sleep/${id}`); +} + +export async function getExerciseIntensity(from: IsoDate, to: IsoDate): Promise<{ date: IsoDate; intensity: 'none' | 'light' | 'hard' }[]> { + return apiGet<{ date: IsoDate; intensity: 'none' | 'light' | 'hard' }[]>(`/api/exercise/intensity?from=${from}&to=${to}`); +} + +export async function upsertExercise(payload: ExerciseUpsert): Promise<{ id: number }> { + return apiPost<{ id: number }>('/api/exercise', payload as unknown as Json); +} diff --git a/sleep-ui/src/lib/components/ConfirmDialog.svelte b/sleep-ui/src/lib/components/ConfirmDialog.svelte new file mode 100644 index 0000000..31eff5e --- /dev/null +++ b/sleep-ui/src/lib/components/ConfirmDialog.svelte @@ -0,0 +1,71 @@ + + + + +{#if open} + +{/if} diff --git a/sleep-ui/src/lib/components/SleepBar.svelte b/sleep-ui/src/lib/components/SleepBar.svelte new file mode 100644 index 0000000..a8aec8d --- /dev/null +++ b/sleep-ui/src/lib/components/SleepBar.svelte @@ -0,0 +1,50 @@ + + +
+ {#each segments as seg} +
+ {/each} +
diff --git a/sleep-ui/src/lib/components/SleepForm.svelte b/sleep-ui/src/lib/components/SleepForm.svelte new file mode 100644 index 0000000..cd8fa89 --- /dev/null +++ b/sleep-ui/src/lib/components/SleepForm.svelte @@ -0,0 +1,227 @@ + + +{#if errorMsg} +
+ {errorMsg} +
+{/if} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + diff --git a/sleep-ui/src/lib/components/WeekRow.svelte b/sleep-ui/src/lib/components/WeekRow.svelte new file mode 100644 index 0000000..2099449 --- /dev/null +++ b/sleep-ui/src/lib/components/WeekRow.svelte @@ -0,0 +1,87 @@ + + +
+
{date}
+ + {#if item} +
+ +
+ Duration: {fmtMin(item.duration_min ?? null)} + Quality: {item.quality} + Latency: {item.latency_min}m + {#if intensity} + Exercise: {intensity} + {/if} +
+
+
+ + +
+ {:else} +
No entry
+
+ +
+ {/if} +
diff --git a/sleep-ui/src/lib/stores/sleep.ts b/sleep-ui/src/lib/stores/sleep.ts new file mode 100644 index 0000000..b63ae68 --- /dev/null +++ b/sleep-ui/src/lib/stores/sleep.ts @@ -0,0 +1,32 @@ +import { writable } from 'svelte/store'; +import type { SleepListItem } from '$lib/api'; + +type Intensity = 'none' | 'light' | 'hard'; + +export const recentSleep = writable([]); + +// Map of date (YYYY-MM-DD) -> intensity +export const exerciseIntensityByDate = writable>({}); + +// Helpers to update stores +export function upsertRecent(item: SleepListItem) { + recentSleep.update((arr) => { + const idx = arr.findIndex((x) => x.date === item.date); + if (idx >= 0) { + const copy = arr.slice(); + copy[idx] = item; + // Keep sort by date desc if already sorted + copy.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); + return copy; + } + return [item, ...arr].sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); + }); +} + +export function removeRecentById(id: number) { + recentSleep.update((arr) => arr.filter((x) => x.id !== id)); +} + +export function setIntensity(date: string, intensity: Intensity) { + exerciseIntensityByDate.update((m) => ({ ...m, [date]: intensity })); +} diff --git a/sleep-ui/src/lib/utils/sleep.ts b/sleep-ui/src/lib/utils/sleep.ts new file mode 100644 index 0000000..d0194a8 --- /dev/null +++ b/sleep-ui/src/lib/utils/sleep.ts @@ -0,0 +1,28 @@ +export type Segment = { start: number; end: number }; // minutes since 00:00 + +export function toMinutes(t: string): number { + const [hh, mm] = t.split(':').map((v) => parseInt(v, 10)); + const hours = Number.isFinite(hh) ? hh : 0; + const mins = Number.isFinite(mm) ? mm : 0; + return hours * 60 + mins; +} + +/** + * Compute 24h track segments for a sleep interval that may cross midnight. + * Inputs are "HH:mm" or "HH:mm:ss" local times. + */ +export function computeSegments(bed_time: string, wake_time: string): Segment[] { + const total = 24 * 60; + const bedMin = toMinutes(bed_time); + const wakeMin = toMinutes(wake_time); + + if (!Number.isFinite(bedMin) || !Number.isFinite(wakeMin)) return []; + if (bedMin <= wakeMin) { + return [{ start: bedMin, end: wakeMin }]; + } + // Wrap across midnight: [0, wake) and [bed, 1440) + return [ + { start: 0, end: wakeMin }, + { start: bedMin, end: total } + ]; +} diff --git a/sleep-ui/src/routes/+layout.svelte b/sleep-ui/src/routes/+layout.svelte index 9303ef2..663a6fe 100644 --- a/sleep-ui/src/routes/+layout.svelte +++ b/sleep-ui/src/routes/+layout.svelte @@ -3,7 +3,7 @@ import { toasts, pushToast, dismissToast } from '$lib/stores/toast'; import { readCsrfToken } from '$lib/api'; import '../app.css'; - const AUTH_PREFIX = import.meta.env.DEV ? '/auth' : ''; + const AUTH_PREFIX = '/api'; export let data: { session?: boolean; pathname?: string }; diff --git a/sleep-ui/src/routes/+page.server.ts b/sleep-ui/src/routes/+page.server.ts new file mode 100644 index 0000000..d0d857d --- /dev/null +++ b/sleep-ui/src/routes/+page.server.ts @@ -0,0 +1,45 @@ + +type SleepListItem = { + id: number; + date: string; // YYYY-MM-DD + bed_time: string; + wake_time: string; + latency_min: number; + awakenings: number; + quality: number; + duration_min: number | null; +}; + +export const load = async ({ fetch }: any) => { + let recent: SleepListItem[] = []; + let intensities: { date: string; intensity: 'none' | 'light' | 'hard' }[] = []; + try { + const res = await fetch('/api/sleep/recent?days=7'); + if (res.ok) { + recent = await res.json(); + } + } catch { + // ignore; treat as empty + } + // Compute last 7-day window [start, end] + function isoDate(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; + } + const endD = new Date(); + const startD = new Date(endD); + startD.setDate(endD.getDate() - 6); + const start = isoDate(startD); + const end = isoDate(endD); + try { + const res2 = await fetch(`/api/exercise/intensity?from=${start}&to=${end}`); + if (res2.ok) { + intensities = await res2.json(); + } + } catch { + // ignore; treat as empty + } + return { recent, intensities }; +}; diff --git a/sleep-ui/src/routes/+page.svelte b/sleep-ui/src/routes/+page.svelte index 0c3991c..e84663e 100644 --- a/sleep-ui/src/routes/+page.svelte +++ b/sleep-ui/src/routes/+page.svelte @@ -1,10 +1,79 @@
-

Welcome

-

- This is the SleepTracker dashboard. Use the navigation to access Trends. -

+
+

This week

+ +
+ +
+ {#each rows as r (r.date)} + + {/each} +
diff --git a/sleep-ui/src/routes/day/[date]/+page.server.ts b/sleep-ui/src/routes/day/[date]/+page.server.ts new file mode 100644 index 0000000..ece9134 --- /dev/null +++ b/sleep-ui/src/routes/day/[date]/+page.server.ts @@ -0,0 +1,25 @@ +type SleepListItem = { + id: number; + date: string; // YYYY-MM-DD + bed_time: string; + wake_time: string; + latency_min: number; + awakenings: number; + quality: number; + duration_min: number | null; +}; + +export const load = async ({ fetch, params }: any) => { + const date = params.date as string; + let items: SleepListItem[] = []; + try { + const res = await fetch(`/api/sleep/range?from=${date}&to=${date}`); + if (res.ok) { + items = await res.json(); + } + } catch { + // ignore + } + const item = items.length > 0 ? items[0] : null; + return { date, item }; +}; diff --git a/sleep-ui/src/routes/day/[date]/+page.svelte b/sleep-ui/src/routes/day/[date]/+page.svelte new file mode 100644 index 0000000..a3cd27a --- /dev/null +++ b/sleep-ui/src/routes/day/[date]/+page.svelte @@ -0,0 +1,62 @@ + + +
+
+

Day view: {data.date}

+ {#if data.item} + + {/if} +
+ + {#if data.item} +
+ +
+
Bed: {data.item.bed_time}
+
Wake: {data.item.wake_time}
+
Duration: {fmtMin(data.item.duration_min)}
+
Latency: {data.item.latency_min}m
+
Awakenings: {data.item.awakenings}
+
Quality: {data.item.quality}
+
+
+ {:else} +
+ No sleep entry for this date. +
+ {/if} +
diff --git a/sleep-ui/src/routes/login/+page.svelte b/sleep-ui/src/routes/login/+page.svelte index b7f41c1..9f62c30 100644 --- a/sleep-ui/src/routes/login/+page.svelte +++ b/sleep-ui/src/routes/login/+page.svelte @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import { pushToast } from '$lib/stores/toast'; - const AUTH_PREFIX = import.meta.env.DEV ? '/auth' : ''; + const AUTH_PREFIX = '/api'; let email = ''; let password = ''; diff --git a/sleep-ui/src/routes/sleep/[id]/edit/+page.server.ts b/sleep-ui/src/routes/sleep/[id]/edit/+page.server.ts new file mode 100644 index 0000000..a4be985 --- /dev/null +++ b/sleep-ui/src/routes/sleep/[id]/edit/+page.server.ts @@ -0,0 +1,30 @@ +import type { PageServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; + +type SleepSession = { + id: number; + date: string; // YYYY-MM-DD + bed_time: string; // HH:mm:ss + wake_time: string; // HH:mm:ss + latency_min: number | null; + awakenings: number | null; + quality: number | null; +}; + +export const load: PageServerLoad = async ({ fetch, params, url }) => { + const id = Number(params.id); + if (!Number.isFinite(id) || id <= 0) { + throw error(400, 'Invalid id'); + } + + const res = await fetch(`/api/sleep/${id}`); + if (!res.ok) { + if (res.status === 404) throw error(404, 'Not found'); + throw error(res.status, `Failed to load record: ${res.status}`); + } + + const rec = (await res.json()) as SleepSession; + const qDate = url.searchParams.get('date'); + + return { rec, qDate }; +}; diff --git a/sleep-ui/src/routes/sleep/[id]/edit/+page.svelte b/sleep-ui/src/routes/sleep/[id]/edit/+page.svelte new file mode 100644 index 0000000..559753b --- /dev/null +++ b/sleep-ui/src/routes/sleep/[id]/edit/+page.svelte @@ -0,0 +1,72 @@ + + +
+
+

Edit sleep entry

+ +
+ + +
diff --git a/sleep-ui/src/routes/sleep/new/+page.svelte b/sleep-ui/src/routes/sleep/new/+page.svelte new file mode 100644 index 0000000..49d0e64 --- /dev/null +++ b/sleep-ui/src/routes/sleep/new/+page.svelte @@ -0,0 +1,18 @@ + + +
+

New sleep entry

+ +
diff --git a/sleep-ui/tests/e2e.spec.ts b/sleep-ui/tests/e2e.spec.ts new file mode 100644 index 0000000..dbd2428 --- /dev/null +++ b/sleep-ui/tests/e2e.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; + +// These must match your backend ADMIN_EMAIL / password. +// Provide via environment variables or skip the test if absent. +const EMAIL = process.env.PLAYWRIGHT_EMAIL; +const PASSWORD = process.env.PLAYWRIGHT_PASSWORD; + +// Helper to skip when creds are not provided +test.skip(!EMAIL || !PASSWORD, 'PLAYWRIGHT_EMAIL and PLAYWRIGHT_PASSWORD are required for this test'); + +test('dashboard quick log -> edit -> delete, with duration warning', async ({ page }) => { + // Login + await page.goto('/login'); + await page.getByLabel('Email').fill(EMAIL!); + await page.getByLabel('Password').fill(PASSWORD!); + await Promise.all([page.waitForURL('**/'), page.getByRole('button', { name: /sign in/i }).click()]); + await expect(page.getByRole('heading', { name: /This week/i })).toBeVisible(); + + // Quick Log: navigate to create form + await page.getByRole('button', { name: /quick log/i }).click(); + await expect(page.getByRole('heading', { name: /new sleep entry/i })).toBeVisible(); + + // Fill minimal valid values that trigger duration warning (< 2h) + // Date defaults to today; leave it. + await page.getByLabel('Bed time').fill('22:00'); + await page.getByLabel('Wake time').fill('23:00'); + await page.getByLabel('Latency (min)').fill('5'); + await page.getByLabel('Awakenings').fill('0'); + await page.getByLabel('Quality (1-5)').fill('3'); + await page.getByLabel('Exercise intensity').selectOption('light'); + + // Submit -> expect ConfirmDialog warning, then proceed + await page.getByRole('button', { name: /^save$/i }).click(); + await expect(page.getByText(/unusual duration/i)).toBeVisible(); + await page.getByRole('button', { name: /proceed/i }).click(); + + // Back to dashboard, row for today should be present + await expect(page.getByRole('heading', { name: /this week/i })).toBeVisible(); + + // Edit the first row (assumes newest first) + await page.getByRole('button', { name: /^edit$/i }).first().click(); + + // Edit page loads via ?date=..., update quality to 5 and save + await expect(page.getByRole('heading', { name: /edit sleep entry/i })).toBeVisible(); + const quality = page.getByLabel('Quality (1-5)'); + await quality.fill('5'); + await page.getByRole('button', { name: /^update$/i }).click(); + + // Back to dashboard; verify that some "Quality: 5" text exists in the first row area + await expect(page.getByText(/Quality:\s*5/i)).toBeVisible(); + + // Delete the entry from dashboard; accept confirm dialog + page.once('dialog', async (dialog) => { + await dialog.accept(); + }); + await page.getByRole('button', { name: /^delete$/i }).first().click(); + + // After deletion, "Quality: 5" might disappear for that row; at least ensure the dashboard still renders + await expect(page.getByRole('heading', { name: /this week/i })).toBeVisible(); +}); diff --git a/sleep-ui/tests/unit/csrf.spec.ts b/sleep-ui/tests/unit/csrf.spec.ts new file mode 100644 index 0000000..dd3f65d --- /dev/null +++ b/sleep-ui/tests/unit/csrf.spec.ts @@ -0,0 +1,38 @@ +// @vitest-environment jsdom +import { describe, it, expect, beforeEach } from 'vitest'; +import { getCsrfToken, readCsrfToken } from '../../src/lib/api'; + +describe('getCsrfToken/readCsrfToken', () => { + beforeEach(() => { + // reset cookies + document.cookie.split(';').forEach((c) => { + const eqPos = c.indexOf('='); + const name = eqPos > -1 ? c.substring(0, eqPos).trim() : c.trim(); + if (name) { + // expire cookie + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`; + } + }); + }); + + it('returns __Host-csrf when present (secure mode)', () => { + // Simulate secure mode by setting only the __Host-csrf cookie. + // jsdom may drop "__Host-" cookies depending on environment; if so, skip assertions. + document.cookie = '__Host-csrf=SECURETOKEN; path=/'; + if (document.cookie.includes('__Host-csrf=')) { + expect(readCsrfToken()).toBe('SECURETOKEN'); + expect(getCsrfToken()).toBe('SECURETOKEN'); + } + }); + + it('falls back to dev csrf cookie when __Host-csrf absent', () => { + document.cookie = 'csrf=DEVTOKEN; path=/'; + expect(readCsrfToken()).toBe('DEVTOKEN'); + expect(getCsrfToken()).toBe('DEVTOKEN'); + }); + + it('returns null when no csrf cookies present', () => { + expect(readCsrfToken()).toBeNull(); + expect(getCsrfToken()).toBeNull(); + }); +}); diff --git a/sleep-ui/tests/unit/sleep.spec.ts b/sleep-ui/tests/unit/sleep.spec.ts new file mode 100644 index 0000000..6442240 --- /dev/null +++ b/sleep-ui/tests/unit/sleep.spec.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { computeSegments } from '../../src/lib/utils/sleep'; + +describe('computeSegments', () => { + it('returns single segment when no wrap (22:00 -> 23:00)', () => { + const segs = computeSegments('22:00:00', '23:00:00'); + expect(segs).toHaveLength(1); + expect(segs[0]).toEqual({ start: 22 * 60, end: 23 * 60 }); + }); + + it('wraps across midnight (23:00 -> 06:00) into two segments', () => { + const segs = computeSegments('23:00:00', '06:00:00'); + expect(segs).toHaveLength(2); + expect(segs[0]).toEqual({ start: 0, end: 6 * 60 }); + expect(segs[1]).toEqual({ start: 23 * 60, end: 24 * 60 }); + }); +}); diff --git a/sleep-ui/vite.config.ts b/sleep-ui/vite.config.ts index 9cf712b..77b8cf4 100644 --- a/sleep-ui/vite.config.ts +++ b/sleep-ui/vite.config.ts @@ -2,22 +2,14 @@ import { sveltekit } from '@sveltejs/kit/vite'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; const target = process.env.PROXY_TARGET ?? 'http://localhost:8080'; +const isTest = process.env.VITEST; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], + plugins: isTest ? [] : [tailwindcss(), sveltekit()], server: { port: 5173, proxy: { '/api': { target, changeOrigin: true }, - '/auth': { - target, - changeOrigin: true, - rewrite: (path) => path.replace(/^\/auth/, '') - }, - '/sleep': { target, changeOrigin: true }, - '/exercise': { target, changeOrigin: true }, - '/note': { target, changeOrigin: true }, - '/health': { target, changeOrigin: true } } } }); diff --git a/sleep-ui/vitest.config.ts b/sleep-ui/vitest.config.ts new file mode 100644 index 0000000..9934605 --- /dev/null +++ b/sleep-ui/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + include: ['tests/unit/**/*.spec.ts'], + exclude: ['node_modules', 'dist'], + }, +});