diff --git a/examples/event-board/common/api/events.js b/examples/event-board/common/api/events.js index c14e7bf1..560477d3 100644 --- a/examples/event-board/common/api/events.js +++ b/examples/event-board/common/api/events.js @@ -19,7 +19,8 @@ const events = [ startsAt: new Date('2026-05-12T18:00:00Z').getTime(), location: 'Online', category: 'workshop', - attendees: 24 + guests: 24, + attendeeIds: [] }, { id: '2', @@ -29,7 +30,8 @@ const events = [ startsAt: new Date('2026-05-21T19:30:00Z').getTime(), location: 'Berlin', category: 'meetup', - attendees: 58 + guests: 58, + attendeeIds: [] }, { id: '3', @@ -39,7 +41,8 @@ const events = [ startsAt: new Date('2026-06-03T17:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 102 + guests: 102, + attendeeIds: [] }, { id: '4', @@ -49,7 +52,8 @@ const events = [ startsAt: new Date('2026-06-11T18:30:00Z').getTime(), location: 'Prague', category: 'meetup', - attendees: 41 + guests: 41, + attendeeIds: [] }, { id: '5', @@ -59,7 +63,8 @@ const events = [ startsAt: new Date('2026-06-24T09:00:00Z').getTime(), location: 'Amsterdam', category: 'conference', - attendees: 180 + guests: 180, + attendeeIds: [] }, { id: '6', @@ -69,7 +74,8 @@ const events = [ startsAt: new Date('2026-07-02T16:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 76 + guests: 76, + attendeeIds: [] } ] @@ -116,6 +122,26 @@ function sortEvents(items) { return [...items].sort((a, b) => a.startsAt - b.startsAt || a.id.localeCompare(b.id)) } +/** + * Convert an internal event record into an API payload for a user. + * @param {object} event - Internal event record. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object} Event payload with `attendees` count and personal `going` flag. + */ +function serializeEvent(event, userId) { + const { + guests, + attendeeIds, + ...publicEvent + } = event + + return { + ...publicEvent, + attendees: guests + attendeeIds.length, + going: userId ? attendeeIds.includes(userId) : false + } +} + function validateEventInput(input) { const errors = {} @@ -155,9 +181,10 @@ function validateEventInput(input) { * @param {string | undefined} query.category - Category filter. * @param {string | undefined} query.cursor - Anchor cursor based on `startsAt`. * @param {string | undefined} query.limit - Page size. + * @param {string | undefined} userId - Current user id, if authenticated. * @returns {{ status: number, body: { events: object[], nextCursor?: number } | { error: string } }} API response payload. */ -export function listEvents(query) { +export function listEvents(query, userId) { const q = query.q?.trim().toLowerCase() const category = normalizeCategory(query.category) const cursor = Number(query.cursor || 0) @@ -193,7 +220,7 @@ export function listEvents(query) { return { status: 200, body: { - events: pageItems, + events: pageItems.map(event => serializeEvent(event, userId)), nextCursor: hasMore ? pageItems[pageItems.length - 1]?.startsAt : undefined } } @@ -202,18 +229,22 @@ export function listEvents(query) { /** * Find an event by slug. * @param {string} slug - Event slug. - * @returns {object | null} Event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Event payload or `null` when it does not exist. */ -export function findEvent(slug) { - return events.find(event => event.slug === slug) || null +export function findEvent(slug, userId) { + const event = events.find(item => item.slug === slug) || null + + return event && serializeEvent(event, userId) } /** * Create a new event in the in-memory store. * @param {object} input - Event form payload. + * @param {object | null} user - Current user, if authenticated. * @returns {{ status: number, body: object }} API response payload. */ -export function createEvent(input) { +export function createEvent(input, user) { const errors = validateEventInput(input) if (Object.keys(errors).length > 0) { @@ -233,30 +264,42 @@ export function createEvent(input) { startsAt: input.startsAt, location: input.location.trim(), category: input.category, - attendees: 0 + author: user?.name, + guests: 0, + attendeeIds: [] } events.push(event) return { status: 201, - body: event + body: serializeEvent(event, user?.id) } } /** - * Increment attendees count for an event. + * Register an RSVP for an event. An authenticated user toggles their personal + * attendance, an anonymous request just increments the guest counter. * @param {string} id - Event id. - * @returns {object | null} Updated event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Updated event payload or `null` when it does not exist. */ -export function rsvpEvent(id) { +export function rsvpEvent(id, userId) { const event = events.find(item => item.id === id) || null if (!event) { return null } - event.attendees += 1 + if (userId) { + if (event.attendeeIds.includes(userId)) { + event.attendeeIds = event.attendeeIds.filter(attendeeId => attendeeId !== userId) + } else { + event.attendeeIds = [...event.attendeeIds, userId] + } + } else { + event.guests += 1 + } - return event + return serializeEvent(event, userId) } diff --git a/examples/event-board/common/api/index.js b/examples/event-board/common/api/index.js index 738b5dcc..51ad77c9 100644 --- a/examples/event-board/common/api/index.js +++ b/examples/event-board/common/api/index.js @@ -1,30 +1,113 @@ import { Hono } from 'hono' +import { + deleteCookie, + getCookie, + setCookie +} from 'hono/cookie' import { createEvent, findEvent, listEvents, rsvpEvent } from './events.js' +import { + authenticateUser, + createSession, + deleteSession, + findUserBySession, + publicUser +} from './users.js' const HTTP_BAD_REQUEST = 400 +const HTTP_UNAUTHORIZED = 401 const HTTP_NOT_FOUND = 404 +const SESSION_COOKIE = 'session' +const SESSION_MAX_AGE = 60 * 60 * 24 * 30 + +/** + * Resolve the current user from the request session cookie. + * @param {import('hono').Context} c + * @returns {object | null} Session user or `null`. + */ +function currentUser(c) { + return findUserBySession(getCookie(c, SESSION_COOKIE)) +} export function api() { const app = new Hono() + app.post('/api/auth/login', async (c) => { + let body + + try { + body = await c.req.json() + } catch { + return c.json({ + error: 'Expected JSON payload' + }, HTTP_BAD_REQUEST) + } + + const user = authenticateUser(body?.username, body?.password) + + if (!user) { + return c.json({ + error: 'Invalid username or password' + }, HTTP_UNAUTHORIZED) + } + + setCookie(c, SESSION_COOKIE, createSession(user.id), { + path: '/', + httpOnly: true, + sameSite: 'Lax', + maxAge: SESSION_MAX_AGE + }) + + return c.json(publicUser(user)) + }) + + app.post('/api/auth/logout', (c) => { + const token = getCookie(c, SESSION_COOKIE) + + if (token) { + deleteSession(token) + } + + deleteCookie(c, SESSION_COOKIE, { + path: '/' + }) + + return c.json({ + ok: true + }) + }) + + app.get('/api/users/me', (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + + return c.json(publicUser(user)) + }) + app.get('/api/events', (c) => { + const user = currentUser(c) const result = listEvents({ q: c.req.query('q'), category: c.req.query('category'), cursor: c.req.query('cursor'), limit: c.req.query('limit') - }) + }, user?.id) return c.json(result.body, result.status) }) app.get('/api/events/:slug', (c) => { - const event = findEvent(c.req.param('slug')) + const user = currentUser(c) + const event = findEvent(c.req.param('slug'), user?.id) if (!event) { return c.json(null, HTTP_NOT_FOUND) @@ -34,6 +117,14 @@ export function api() { }) app.post('/api/events', async (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + let body try { @@ -46,13 +137,14 @@ export function api() { }, HTTP_BAD_REQUEST) } - const result = createEvent(body) + const result = createEvent(body, user) return c.json(result.body, result.status) }) app.post('/api/events/:id/rsvp', (c) => { - const event = rsvpEvent(c.req.param('id')) + const user = currentUser(c) + const event = rsvpEvent(c.req.param('id'), user?.id) if (!event) { return c.json(null, HTTP_NOT_FOUND) diff --git a/examples/event-board/common/api/users.js b/examples/event-board/common/api/users.js new file mode 100644 index 00000000..4b7c6610 --- /dev/null +++ b/examples/event-board/common/api/users.js @@ -0,0 +1,87 @@ +/** + * Demo users for the mock API. Credentials are shown on the login form. + */ +const users = [ + { + id: 'u1', + username: 'ada', + password: 'lovelace', + name: 'Ada Lovelace' + }, + { + id: 'u2', + username: 'grace', + password: 'hopper', + name: 'Grace Hopper' + } +] +/** + * In-memory session storage: token -> user id. + * Sessions reset on server restart, same as the rest of the mock data. + */ +const sessions = new Map() + +/** + * Find a user by credentials. + * @param {string} username + * @param {string} password + * @returns {object | null} Matched user or `null` when credentials are invalid. + */ +export function authenticateUser(username, password) { + if (typeof username !== 'string' || typeof password !== 'string') { + return null + } + + return users.find( + user => user.username === username && user.password === password + ) || null +} + +/** + * Create a session for a user. + * @param {string} userId + * @returns {string} Session token. + */ +export function createSession(userId) { + const token = crypto.randomUUID() + + sessions.set(token, userId) + + return token +} + +/** + * Delete a session by token. + * @param {string} token + */ +export function deleteSession(token) { + sessions.delete(token) +} + +/** + * Find a user by session token. + * @param {string | undefined} token + * @returns {object | null} Session user or `null` when the session is invalid. + */ +export function findUserBySession(token) { + const userId = token && sessions.get(token) + + if (!userId) { + return null + } + + return users.find(user => user.id === userId) || null +} + +/** + * Strip private fields from a user. + * @param {object} user + * @returns {{ id: string, username: string, name: string }} Public user payload. + */ +export function publicUser(user) { + return { + id: user.id, + username: user.username, + name: user.name + } +} diff --git a/examples/event-board/common/oxlint.config.ts b/examples/event-board/common/oxlint.config.ts index d6356252..a32628cb 100644 --- a/examples/event-board/common/oxlint.config.ts +++ b/examples/event-board/common/oxlint.config.ts @@ -5,6 +5,12 @@ import testConfig from '@trigen/oxlint-config/test' import rootConfig from '../../../oxlint.config.ts' export default defineConfig({ + // These files import the variant-local intl service/store, which does not + // exist in common — they are linted in each variant after `pnpm sync` + ignorePatterns: [ + 'src/services/api/api.server.ts', + 'src/stores-di/user.ts' + ], extends: [ rootConfig, bundlerConfig, diff --git a/examples/event-board/common/package.json b/examples/event-board/common/package.json index bc4a6b92..6b988547 100644 --- a/examples/event-board/common/package.json +++ b/examples/event-board/common/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@hono/node-server": "^1.19.9", + "@nano_kit/platform-web": "*", "@nano_kit/query": "*", "@nano_kit/router": "*", "@nano_kit/store": "*", diff --git a/examples/event-board/common/src/services/api/api.client.ts b/examples/event-board/common/src/services/api/api.client.ts new file mode 100644 index 00000000..d1cc0039 --- /dev/null +++ b/examples/event-board/common/src/services/api/api.client.ts @@ -0,0 +1,22 @@ +import { Injectable$ } from '@nano_kit/store' +import type { ApiService$ } from './api' + +/** + * Browser API transport. Requests stay relative to the current origin, so + * the browser attaches cookies and `Accept-Language` on its own. + */ +export class ClientApiService$ extends Injectable$ implements ApiService$ { + fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/common/src/services/api/api.server.ts b/examples/event-board/common/src/services/api/api.server.ts new file mode 100644 index 00000000..2675e09b --- /dev/null +++ b/examples/event-board/common/src/services/api/api.server.ts @@ -0,0 +1,62 @@ +import { + CookieStore$, + serializeCookies +} from '@nano_kit/platform-web' +import { + Injectable$, + inject +} from '@nano_kit/store' +import { IntlService$ } from '../intl' +import type { ApiService$ } from './api' + +/** + * Absolute API origin used by the server renderer. + * Browser requests stay relative to the current origin. + */ +const API_ORIGIN = import.meta.env.VITE_EVENT_BOARD_API_ORIGIN + || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') + +/** + * SSR API transport. Server-to-server requests carry no browser context, so + * the incoming request cookies and locale are forwarded explicitly. + */ +export class ServerApiService$ extends Injectable$ implements ApiService$ { + cookieStore = inject(CookieStore$) + intlService = inject(IntlService$) + + private async getHeaders() { + const headers: Record = {} + const [ + cookie, + locale + ] = await Promise.all([ + serializeCookies(this.cookieStore, ['session']), + this.intlService.getServerLocale() + ]) + + if (cookie) { + headers['Cookie'] = cookie + } + + if (locale) { + headers['Accept-Language'] = locale + } + + return headers + } + + async fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`${API_ORIGIN}/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...await this.getHeaders(), + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/common/src/services/api/api.ts b/examples/event-board/common/src/services/api/api.ts new file mode 100644 index 00000000..59fffb3a --- /dev/null +++ b/examples/event-board/common/src/services/api/api.ts @@ -0,0 +1,17 @@ +import { ClientApiService$ } from './api.client' +import { ServerApiService$ } from './api.server' + +export interface ApiService$ { + /** + * Fetch wrapper that adds the API base URL, `Content-Type` and forwarded + * request headers + * @param url + * @param options + * @returns Fetch response + */ + fetch(url: string, options?: RequestInit): Promise +} + +export const ApiService$ = import.meta.env.SSR + ? ServerApiService$ + : ClientApiService$ diff --git a/examples/event-board/common/src/services/api/api.types.ts b/examples/event-board/common/src/services/api/api.types.ts new file mode 100644 index 00000000..759840fb --- /dev/null +++ b/examples/event-board/common/src/services/api/api.types.ts @@ -0,0 +1,5 @@ +export const HttpStatus = { + BadRequest: 400, + Unauthorized: 401, + NotFound: 404 +} as const diff --git a/examples/event-board/common/src/services/api/index.ts b/examples/event-board/common/src/services/api/index.ts new file mode 100644 index 00000000..f133e643 --- /dev/null +++ b/examples/event-board/common/src/services/api/index.ts @@ -0,0 +1,2 @@ +export * from './api' +export * from './api.types' diff --git a/examples/event-board/common/src/services/events.ts b/examples/event-board/common/src/services/events.ts index 0b5aebfd..f359daf4 100644 --- a/examples/event-board/common/src/services/events.ts +++ b/examples/event-board/common/src/services/events.ts @@ -1,107 +1,107 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' import type { BoardEvent, EventsFilter, EventsPage, NewEventForm } from './events.types' +import { + ApiService$, + HttpStatus +} from './api' export * from './events.types' -/** - * Absolute API origin used by the server renderer. - * Browser requests stay relative to the current origin. - */ -const API_ORIGIN = import.meta.env.SSR - ? import.meta.env.VITE_EVENT_BOARD_API_ORIGIN || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') - : '' -const HTTP_NOT_FOUND = 404 - -/** - * Fetch a cursor-paginated page of events. - * @param filter - Search, category, cursor, and limit filters. - * @returns Events page with optional next cursor. - */ -export async function fetchEvents(filter: EventsFilter = {}) { - const params = new URLSearchParams() - - if (filter.q) { - params.set('q', filter.q) - } +export class EventsService$ extends Injectable$ { + api = inject(ApiService$) - if (filter.category) { - params.set('category', filter.category) - } + /** + * Fetch a cursor-paginated page of events. + * @param filter - Search, category, cursor, and limit filters. + * @returns Events page with optional next cursor. + */ + async fetchEvents(filter: EventsFilter = {}) { + const params = new URLSearchParams() - if (filter.cursor) { - params.set('cursor', String(filter.cursor)) - } + if (filter.q) { + params.set('q', filter.q) + } - if (filter.limit) { - params.set('limit', String(filter.limit)) - } + if (filter.category) { + params.set('category', filter.category) + } - const response = await fetch(`${API_ORIGIN}/api/events?${params}`) + if (filter.cursor) { + params.set('cursor', String(filter.cursor)) + } - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + if (filter.limit) { + params.set('limit', String(filter.limit)) + } - return await response.json() as EventsPage -} + const response = await this.api.fetch(`events?${params}`) -/** - * Fetch a single event by slug. - * @param slug - Event slug from the route. - * @returns Event details, or `null` when the event does not exist. - */ -export async function fetchEvent(slug: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${slug}`) + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } - if (response.status === HTTP_NOT_FOUND) { - return null + return await response.json() as EventsPage } - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + /** + * Fetch a single event by slug. + * @param slug - Event slug from the route. + * @returns Event details, or `null` when the event does not exist. + */ + async fetchEvent(slug: string) { + const response = await this.api.fetch(`events/${slug}`) - return await response.json() as BoardEvent -} + if (response.status === HttpStatus.NotFound) { + return null + } -/** - * Create a new event in the mock API. - * @param event - New event form payload. - * @returns Created event with generated id, slug, and attendees count. - */ -export async function createEvent(event: NewEventForm) { - const response = await fetch(`${API_ORIGIN}/api/events`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(event) - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } - return await response.json() as BoardEvent -} + return await response.json() as BoardEvent + } -/** - * Mark the current user as going to an event. - * @param id - Event id. - * @returns Updated event with incremented attendees count. - */ -export async function rsvpEvent(id: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${id}/rsvp`, { - method: 'POST' - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) + /** + * Create a new event in the mock API. + * @param event - New event form payload. + * @returns Created event with generated id, slug, and attendees count. + */ + async createEvent(event: NewEventForm) { + const response = await this.api.fetch('events', { + method: 'POST', + body: JSON.stringify(event) + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent } - return await response.json() as BoardEvent + /** + * Mark the current user as going to an event. + * @param id - Event id. + * @returns Updated event with incremented attendees count. + */ + async rsvpEvent(id: string) { + const response = await this.api.fetch(`events/${id}/rsvp`, { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent + } } diff --git a/examples/event-board/common/src/services/events.types.ts b/examples/event-board/common/src/services/events.types.ts index ce3f3fa3..1d54d127 100644 --- a/examples/event-board/common/src/services/events.types.ts +++ b/examples/event-board/common/src/services/events.types.ts @@ -15,7 +15,9 @@ export interface BoardEvent { startsAt: number location: string category: EventCategory + author?: string attendees: number + going: boolean } export interface EventsPage { diff --git a/examples/event-board/common/src/services/user.ts b/examples/event-board/common/src/services/user.ts new file mode 100644 index 00000000..2a4c7960 --- /dev/null +++ b/examples/event-board/common/src/services/user.ts @@ -0,0 +1,73 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' +import type { + LoginParams, + User +} from './user.types' +import { + ApiService$, + HttpStatus +} from './api' +import { UserError } from './user.types' + +export * from './user.types' + +export class UserService$ extends Injectable$ { + api = inject(ApiService$) + + /** + * Fetch the current session user. + * @returns Public user payload. + */ + async getUser() { + const response = await this.api.fetch('users/me') + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.Unauthenticated) + } + + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as User + } + + /** + * Login with demo credentials. The API responds with a `Set-Cookie` header, + * so the session cookie is stored by the browser (or by the SSR cookie store). + * @param params - Username and password. + * @returns Public user payload. + */ + async login(params: LoginParams) { + const response = await this.api.fetch('auth/login', { + method: 'POST', + body: JSON.stringify(params) + }) + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.LoginInvalidCredentials) + } + + throw new Error(UserError.LoginFailed) + } + + return await response.json() as User + } + + /** + * Logout the current session. The API deletes the session cookie. + */ + async logout() { + const response = await this.api.fetch('auth/logout', { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + } +} diff --git a/examples/event-board/common/src/services/user.types.ts b/examples/event-board/common/src/services/user.types.ts new file mode 100644 index 00000000..ae9e3454 --- /dev/null +++ b/examples/event-board/common/src/services/user.types.ts @@ -0,0 +1,18 @@ +export const UserError = { + Unauthenticated: 'UNAUTHENTICATED', + LoginFailed: 'LOGIN_FAILED', + LoginInvalidCredentials: 'LOGIN_INVALID_CREDENTIALS' +} as const + +export type UserError = typeof UserError[keyof typeof UserError] + +export interface User { + id: string + username: string + name: string +} + +export interface LoginParams { + username: string + password: string +} diff --git a/examples/event-board/common/src/stores-di/events.ts b/examples/event-board/common/src/stores-di/events.ts index 183fc862..cf6685fb 100644 --- a/examples/event-board/common/src/stores-di/events.ts +++ b/examples/event-board/common/src/stores-di/events.ts @@ -25,13 +25,11 @@ import { type EventCategory, type EventsPage, type NewEventForm, - createEvent, - fetchEvent, - fetchEvents, - rsvpEvent + EventsService$ } from '#src/services/events' import { Params$ } from './router' import { Client$ } from './query' +import { User$ } from './user' import { datetimeLocalValue } from './utils' const SEARCH_DEBOUNCE = 600 @@ -49,6 +47,7 @@ export const EventKey = queryKey<[slug: string | undefined], BoardEvent | null>( export const EventEntity = entity('event') export function EventsList$() { + const eventsService = inject(EventsService$) const { $data, infinite @@ -76,7 +75,7 @@ export function EventsList$() { }) }) - return fetchEvents({ + return eventsService.fetchEvents({ cursor, q, category @@ -102,6 +101,7 @@ export function EventsList$() { } export function EventDetails$() { + const eventsService = inject(EventsService$) const { query } = inject(Client$) const { $slug } = inject(Params$) const [ @@ -113,7 +113,7 @@ export function EventDetails$() { [$slug], async slug => ( slug - ? await fetchEvent(slug) + ? await eventsService.fetchEvent(slug) : null ), [entities(EventEntity)] @@ -127,6 +127,7 @@ export function EventDetails$() { } export function NewEventForm$() { + const eventsService = inject(EventsService$) const { mutation, revalidate @@ -198,7 +199,7 @@ export function NewEventForm$() { })) }) - return createEvent(payload) + return eventsService.createEvent(payload) }, [ disabled(not($valid)), @@ -224,6 +225,8 @@ export function NewEventForm$() { } export function RsvpEvent$() { + const eventsService = inject(EventsService$) + const { $user } = inject(User$) const { $data, mutation @@ -235,21 +238,15 @@ export function RsvpEvent$() { $rsvpLoading ] = mutation<[id: string], BoardEvent>( (id, ctx) => { - const key = EventEntity(id) - const previousEvent = $data(key) - - if (previousEvent) { - $data(key, { - ...previousEvent, - attendees: previousEvent.attendees + 1 - }) + const revert = $data(EventEntity(id), event => event && { + ...event, + going: Boolean($user()) && !event.going, + attendees: event.attendees + (event.going ? -1 : 1) + }) - onError(ctx, () => { - $data(key, previousEvent) - }) - } + onError(ctx, revert) - return rsvpEvent(id) + return eventsService.rsvpEvent(id) } ) diff --git a/examples/event-board/common/src/stores-di/router.ts b/examples/event-board/common/src/stores-di/router.ts index a5cb81cf..fa718013 100644 --- a/examples/event-board/common/src/stores-di/router.ts +++ b/examples/event-board/common/src/stores-di/router.ts @@ -12,10 +12,17 @@ import { export const routes = { home: '/', + login: '/login', newEvent: '/events/new', event: '/events/:slug' } as const +export const publicRoutes = new Set([ + 'home', + 'login', + 'event' +]) + export function Params$() { const $location = inject(Location$) const $searchParams = searchParams($location) diff --git a/examples/event-board/common/src/stores-di/user.ts b/examples/event-board/common/src/stores-di/user.ts new file mode 100644 index 00000000..87dad0f2 --- /dev/null +++ b/examples/event-board/common/src/stores-di/user.ts @@ -0,0 +1,80 @@ +import { + inject, + onMountEffect +} from '@nano_kit/store' +import { + keys, + onSuccess, + queryKey +} from '@nano_kit/query' +import { + LocationNavigation$, + Paths$ +} from '@nano_kit/router' +import { + type LoginParams, + type User, + UserError, + UserService$ +} from '#src/services/user' +import { Client$ } from './query' +import { TranslationsKey } from './intl' +import { publicRoutes } from './router' + +export const UserKey = queryKey<[], User>('user') + +export function User$() { + const userService = inject(UserService$) + const [$location, navigation] = inject(LocationNavigation$) + const paths = inject(Paths$) + const { + query, + mutation, + invalidate + } = inject(Client$) + const [ + $user, + $userError, + $userLoading + ] = query(UserKey, [], () => userService.getUser()) + const invalidateSession = () => { + keys(Key => Key !== TranslationsKey && invalidate(Key)) + } + const [ + login, , + $loginError, + $loginLoading + ] = mutation<[params: LoginParams], User>( + (params, ctx) => { + onSuccess(ctx, () => { + invalidateSession() + navigation.replace(paths.home) + }) + + return userService.login(params) + } + ) + const logout = async () => { + await userService.logout() + invalidateSession() + } + + onMountEffect($user, () => { + const route = $location.$route() + const userError = $userError() + + if (userError === UserError.Unauthenticated && route && !publicRoutes.has(route)) { + navigation.replace(paths.login) + } + }) + + return { + login, + logout, + $user, + $userError, + $userLoading, + $loginError, + $loginLoading + } +} diff --git a/examples/event-board/common/test/api.spec.ts b/examples/event-board/common/test/api.spec.ts new file mode 100644 index 00000000..d9917262 --- /dev/null +++ b/examples/event-board/common/test/api.spec.ts @@ -0,0 +1,238 @@ +import { + describe, + it, + expect +} from 'vitest' +import { api } from '../api/index.js' + +interface ApiUser { + id: string + username: string + name: string +} + +interface ApiEvent { + id: string + slug: string + title: string + author?: string + attendees: number + going: boolean +} + +const app = api() + +function sessionCookie(response: Response) { + return response.headers.get('set-cookie')?.split(';')[0] ?? '' +} + +async function login(username: string, password: string) { + const response = await app.request('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username, + password + }) + }) + + return { + response, + cookie: sessionCookie(response) + } +} + +describe('Event Board API', () => { + describe('auth', () => { + it('should login a demo user and set an http-only session cookie', async () => { + const { response } = await login('ada', 'lovelace') + const user = await response.json() as ApiUser & { + password?: string + } + const setCookie = response.headers.get('set-cookie') ?? '' + + expect(response.status).toBe(200) + expect(user).toMatchObject({ + username: 'ada', + name: 'Ada Lovelace' + }) + expect(user.password).toBeUndefined() + expect(setCookie).toMatch(/session=[^;]+/) + expect(setCookie).toMatch(/HttpOnly/i) + }) + + it('should reject invalid credentials', async () => { + const { response } = await login('ada', 'wrong-password') + + expect(response.status).toBe(401) + expect(await response.json()).toEqual({ + error: 'Invalid username or password' + }) + }) + + it('should return the current user for a valid session', async () => { + const { cookie } = await login('grace', 'hopper') + const response = await app.request('/api/users/me', { + headers: { + Cookie: cookie + } + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ + username: 'grace', + name: 'Grace Hopper' + }) + }) + + it('should respond with 401 to me without a session', async () => { + const response = await app.request('/api/users/me') + + expect(response.status).toBe(401) + }) + + it('should clear the session on logout', async () => { + const { cookie } = await login('ada', 'lovelace') + const logoutResponse = await app.request('/api/auth/logout', { + method: 'POST', + headers: { + Cookie: cookie + } + }) + const meResponse = await app.request('/api/users/me', { + headers: { + Cookie: cookie + } + }) + + expect(logoutResponse.status).toBe(200) + expect(logoutResponse.headers.get('set-cookie')).toMatch(/session=;/) + expect(meResponse.status).toBe(401) + }) + }) + + describe('events', () => { + it('should include attendees count and personal going flag in the list', async () => { + const response = await app.request('/api/events?limit=20') + const { events } = await response.json() as { + events: ApiEvent[] + } + + expect(response.status).toBe(200) + expect(events.length).toBeGreaterThan(0) + + for (const event of events) { + expect(typeof event.attendees).toBe('number') + expect(event.going).toBe(false) + } + }) + + it('should toggle personal attendance for an authenticated rsvp', async () => { + const { cookie } = await login('grace', 'hopper') + const detailsResponse = await app.request('/api/events/vite-plugin-night') + const event = await detailsResponse.json() as ApiEvent + const rsvpResponse = await app.request(`/api/events/${event.id}/rsvp`, { + method: 'POST', + headers: { + Cookie: cookie + } + }) + const rsvped = await rsvpResponse.json() as ApiEvent + const secondRsvpResponse = await app.request(`/api/events/${event.id}/rsvp`, { + method: 'POST', + headers: { + Cookie: cookie + } + }) + const unrsvped = await secondRsvpResponse.json() as ApiEvent + + expect(rsvped.going).toBe(true) + expect(rsvped.attendees).toBe(event.attendees + 1) + expect(unrsvped.going).toBe(false) + expect(unrsvped.attendees).toBe(event.attendees) + }) + + it('should increment the guest counter for an anonymous rsvp', async () => { + const detailsResponse = await app.request('/api/events/web-platform-conference') + const event = await detailsResponse.json() as ApiEvent + const rsvpResponse = await app.request(`/api/events/${event.id}/rsvp`, { + method: 'POST' + }) + const rsvped = await rsvpResponse.json() as ApiEvent + + expect(rsvped.going).toBe(false) + expect(rsvped.attendees).toBe(event.attendees + 1) + }) + + it('should keep the personal attendance visible in event details', async () => { + const { cookie } = await login('ada', 'lovelace') + const detailsResponse = await app.request('/api/events/hydration-deep-dive') + const event = await detailsResponse.json() as ApiEvent + + await app.request(`/api/events/${event.id}/rsvp`, { + method: 'POST', + headers: { + Cookie: cookie + } + }) + + const authedResponse = await app.request('/api/events/hydration-deep-dive', { + headers: { + Cookie: cookie + } + }) + const authedEvent = await authedResponse.json() as ApiEvent + const anonymousResponse = await app.request('/api/events/hydration-deep-dive') + const anonymousEvent = await anonymousResponse.json() as ApiEvent + + expect(authedEvent.going).toBe(true) + expect(authedEvent.attendees).toBe(event.attendees + 1) + expect(anonymousEvent.going).toBe(false) + expect(anonymousEvent.attendees).toBe(event.attendees + 1) + }) + + it('should store the author for an event created with a session', async () => { + const { cookie } = await login('ada', 'lovelace') + const response = await app.request('/api/events', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Cookie': cookie + }, + body: JSON.stringify({ + title: 'Authored Event', + description: 'An event created by a logged in user.', + startsAt: new Date('2026-08-01T18:00:00Z').getTime(), + location: 'Online', + category: 'meetup' + }) + }) + const event = await response.json() as ApiEvent + + expect(response.status).toBe(201) + expect(event.author).toBe('Ada Lovelace') + expect(event.attendees).toBe(0) + expect(event.going).toBe(false) + }) + + it('should reject event creation without a session', async () => { + const response = await app.request('/api/events', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + title: 'Anonymous Event', + description: 'An event created without a session.', + startsAt: new Date('2026-08-02T18:00:00Z').getTime(), + location: 'Online', + category: 'webinar' + }) + }) + + expect(response.status).toBe(401) + }) + }) +}) diff --git a/examples/event-board/common/test/integration.spec.ts b/examples/event-board/common/test/integration.spec.ts index 0ebb7682..cc99d26e 100644 --- a/examples/event-board/common/test/integration.spec.ts +++ b/examples/event-board/common/test/integration.spec.ts @@ -693,6 +693,66 @@ describe('Event Board App', async () => { }).toBe(initialCount + 1) }) + it('should redirect anonymous visitor from new event page to login', async () => { + const response = await openPage(page, url, '/events/new') + const html = await response?.text() + + expect(html).toContain('Log in | Event Board') + + await expect.poll(() => new URL(page.url()).pathname, { + timeout: UI_TIMEOUT + }).toBe('/login') + await page.getByRole('heading', { + name: 'Log in' + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + }) + + it('should redirect anonymous visitor to login on client-side navigation', async () => { + await openPage(page, url) + await page.locator('article.event-card').first().waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + await page.getByRole('link', { + name: 'New event' + }).click() + + await expect.poll(() => new URL(page.url()).pathname, { + timeout: UI_TIMEOUT + }).toBe('/login') + await page.getByRole('heading', { + name: 'Log in' + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + }) + + it('should login a demo user', async () => { + await page.getByLabel('Username').fill('ada') + await page.getByLabel('Password').fill('lovelace') + await page.getByRole('button', { + name: 'Log in' + }).click() + + await expect.poll(() => new URL(page.url()).pathname, { + timeout: UI_TIMEOUT + }).toBe('/') + await page.getByText('Ada Lovelace').waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + await page.getByRole('button', { + name: 'Log out' + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + }) + it('should render new event page', async () => { const response = await openPage(page, url, '/events/new') const html = await response?.text() @@ -847,6 +907,85 @@ describe('Event Board App', async () => { } }) + it('should server render the logged in user from the session cookie', async () => { + const response = await openPage(page, url) + const html = await response?.text() + + expect(html).toContain('Ada Lovelace') + expect(html).toContain('Log out') + }) + + it('should toggle personal attendance from the event page', async () => { + await openPage(page, url, '/events/react-ssr-workshop') + + const attendees = page.locator('.details-panel dd').last() + const initialCount = await numberText(attendees) + + await page.getByRole('button', { + name: "I'm going" + }).click() + + await expect.poll(() => numberText(attendees), { + timeout: UI_TIMEOUT + }).toBe(initialCount + 1) + await page.getByRole('button', { + name: "I'm not going" + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + + const response = await openPage(page, url, '/events/react-ssr-workshop') + const html = (await response?.text())?.replace(/'|'/g, "'") + + expect(html).toContain("I'm not going") + + await page.getByRole('button', { + name: "I'm not going" + }).click() + + await expect.poll(() => numberText(attendees), { + timeout: UI_TIMEOUT + }).toBe(initialCount) + await page.getByRole('button', { + name: "I'm going" + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + }) + + it('should revert optimistic attendance on rsvp failure', async () => { + await openPage(page, url, '/events/react-ssr-workshop') + + const attendees = page.locator('.details-panel dd').last() + const initialCount = await numberText(attendees) + + await page.route('**/api/events/*/rsvp', route => route.abort()) + + try { + await page.getByRole('button', { + name: "I'm going" + }).click() + + await page.locator('.details-panel .notice_error').waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + await expect.poll(() => numberText(attendees), { + timeout: UI_TIMEOUT + }).toBe(initialCount) + await page.getByRole('button', { + name: "I'm going" + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + } finally { + await page.unroute('**/api/events/*/rsvp') + } + }) + it('should navigate top nav in browser', async () => { const documentRequests: string[] = [] const preloadRequests: string[] = [] @@ -996,7 +1135,24 @@ describe('Event Board App', async () => { } }) - it('should return not found for unknown routes', async () => { + it('should logout from the header', async () => { + await openPage(page, url) + await page.getByRole('button', { + name: 'Log out' + }).click() + + await page.getByRole('link', { + name: 'Log in' + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + await expect.poll(() => page.getByText('Ada Lovelace').count(), { + timeout: UI_TIMEOUT + }).toBe(0) + }) + + it('should render styled not found page for unknown routes', async () => { const resolvedUrl = await url if (!resolvedUrl) { @@ -1007,8 +1163,23 @@ describe('Event Board App', async () => { const text = await response.text() expect(response.status).toBe(404) - expect(text).toContain('Not Found') - expect(text).not.toContain('Event Board') + expect(text).toContain('Page not found | Event Board') + expect(text).toContain('Page not found') + expect(text).toContain('Event Board') + + await openPage(page, url, '/not-a-real-route') + await page.getByRole('heading', { + name: 'Page not found' + }).waitFor({ + state: 'visible', + timeout: UI_TIMEOUT + }) + await page.getByRole('link', { + name: 'Back to events' + }).click() + await expect.poll(() => new URL(page.url()).pathname, { + timeout: UI_TIMEOUT + }).toBe('/') }) if (isIntl(name)) { diff --git a/examples/event-board/preact-nano_kit-intl-ssr/.env.example b/examples/event-board/preact-nano_kit-intl-ssr/.env.example new file mode 100644 index 00000000..bfd7e450 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/.env.example @@ -0,0 +1,6 @@ +# Absolute API origin for SSR fetches. +# Defaults: http://localhost:5173 in dev, http://localhost:3001 in production. +VITE_EVENT_BOARD_API_ORIGIN= + +# Production server port (default 3001). +PORT=3001 diff --git a/examples/event-board/preact-nano_kit-intl-ssr/api/events.js b/examples/event-board/preact-nano_kit-intl-ssr/api/events.js index c14e7bf1..560477d3 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/api/events.js +++ b/examples/event-board/preact-nano_kit-intl-ssr/api/events.js @@ -19,7 +19,8 @@ const events = [ startsAt: new Date('2026-05-12T18:00:00Z').getTime(), location: 'Online', category: 'workshop', - attendees: 24 + guests: 24, + attendeeIds: [] }, { id: '2', @@ -29,7 +30,8 @@ const events = [ startsAt: new Date('2026-05-21T19:30:00Z').getTime(), location: 'Berlin', category: 'meetup', - attendees: 58 + guests: 58, + attendeeIds: [] }, { id: '3', @@ -39,7 +41,8 @@ const events = [ startsAt: new Date('2026-06-03T17:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 102 + guests: 102, + attendeeIds: [] }, { id: '4', @@ -49,7 +52,8 @@ const events = [ startsAt: new Date('2026-06-11T18:30:00Z').getTime(), location: 'Prague', category: 'meetup', - attendees: 41 + guests: 41, + attendeeIds: [] }, { id: '5', @@ -59,7 +63,8 @@ const events = [ startsAt: new Date('2026-06-24T09:00:00Z').getTime(), location: 'Amsterdam', category: 'conference', - attendees: 180 + guests: 180, + attendeeIds: [] }, { id: '6', @@ -69,7 +74,8 @@ const events = [ startsAt: new Date('2026-07-02T16:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 76 + guests: 76, + attendeeIds: [] } ] @@ -116,6 +122,26 @@ function sortEvents(items) { return [...items].sort((a, b) => a.startsAt - b.startsAt || a.id.localeCompare(b.id)) } +/** + * Convert an internal event record into an API payload for a user. + * @param {object} event - Internal event record. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object} Event payload with `attendees` count and personal `going` flag. + */ +function serializeEvent(event, userId) { + const { + guests, + attendeeIds, + ...publicEvent + } = event + + return { + ...publicEvent, + attendees: guests + attendeeIds.length, + going: userId ? attendeeIds.includes(userId) : false + } +} + function validateEventInput(input) { const errors = {} @@ -155,9 +181,10 @@ function validateEventInput(input) { * @param {string | undefined} query.category - Category filter. * @param {string | undefined} query.cursor - Anchor cursor based on `startsAt`. * @param {string | undefined} query.limit - Page size. + * @param {string | undefined} userId - Current user id, if authenticated. * @returns {{ status: number, body: { events: object[], nextCursor?: number } | { error: string } }} API response payload. */ -export function listEvents(query) { +export function listEvents(query, userId) { const q = query.q?.trim().toLowerCase() const category = normalizeCategory(query.category) const cursor = Number(query.cursor || 0) @@ -193,7 +220,7 @@ export function listEvents(query) { return { status: 200, body: { - events: pageItems, + events: pageItems.map(event => serializeEvent(event, userId)), nextCursor: hasMore ? pageItems[pageItems.length - 1]?.startsAt : undefined } } @@ -202,18 +229,22 @@ export function listEvents(query) { /** * Find an event by slug. * @param {string} slug - Event slug. - * @returns {object | null} Event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Event payload or `null` when it does not exist. */ -export function findEvent(slug) { - return events.find(event => event.slug === slug) || null +export function findEvent(slug, userId) { + const event = events.find(item => item.slug === slug) || null + + return event && serializeEvent(event, userId) } /** * Create a new event in the in-memory store. * @param {object} input - Event form payload. + * @param {object | null} user - Current user, if authenticated. * @returns {{ status: number, body: object }} API response payload. */ -export function createEvent(input) { +export function createEvent(input, user) { const errors = validateEventInput(input) if (Object.keys(errors).length > 0) { @@ -233,30 +264,42 @@ export function createEvent(input) { startsAt: input.startsAt, location: input.location.trim(), category: input.category, - attendees: 0 + author: user?.name, + guests: 0, + attendeeIds: [] } events.push(event) return { status: 201, - body: event + body: serializeEvent(event, user?.id) } } /** - * Increment attendees count for an event. + * Register an RSVP for an event. An authenticated user toggles their personal + * attendance, an anonymous request just increments the guest counter. * @param {string} id - Event id. - * @returns {object | null} Updated event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Updated event payload or `null` when it does not exist. */ -export function rsvpEvent(id) { +export function rsvpEvent(id, userId) { const event = events.find(item => item.id === id) || null if (!event) { return null } - event.attendees += 1 + if (userId) { + if (event.attendeeIds.includes(userId)) { + event.attendeeIds = event.attendeeIds.filter(attendeeId => attendeeId !== userId) + } else { + event.attendeeIds = [...event.attendeeIds, userId] + } + } else { + event.guests += 1 + } - return event + return serializeEvent(event, userId) } diff --git a/examples/event-board/preact-nano_kit-intl-ssr/api/index.js b/examples/event-board/preact-nano_kit-intl-ssr/api/index.js index 738b5dcc..51ad77c9 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/api/index.js +++ b/examples/event-board/preact-nano_kit-intl-ssr/api/index.js @@ -1,30 +1,113 @@ import { Hono } from 'hono' +import { + deleteCookie, + getCookie, + setCookie +} from 'hono/cookie' import { createEvent, findEvent, listEvents, rsvpEvent } from './events.js' +import { + authenticateUser, + createSession, + deleteSession, + findUserBySession, + publicUser +} from './users.js' const HTTP_BAD_REQUEST = 400 +const HTTP_UNAUTHORIZED = 401 const HTTP_NOT_FOUND = 404 +const SESSION_COOKIE = 'session' +const SESSION_MAX_AGE = 60 * 60 * 24 * 30 + +/** + * Resolve the current user from the request session cookie. + * @param {import('hono').Context} c + * @returns {object | null} Session user or `null`. + */ +function currentUser(c) { + return findUserBySession(getCookie(c, SESSION_COOKIE)) +} export function api() { const app = new Hono() + app.post('/api/auth/login', async (c) => { + let body + + try { + body = await c.req.json() + } catch { + return c.json({ + error: 'Expected JSON payload' + }, HTTP_BAD_REQUEST) + } + + const user = authenticateUser(body?.username, body?.password) + + if (!user) { + return c.json({ + error: 'Invalid username or password' + }, HTTP_UNAUTHORIZED) + } + + setCookie(c, SESSION_COOKIE, createSession(user.id), { + path: '/', + httpOnly: true, + sameSite: 'Lax', + maxAge: SESSION_MAX_AGE + }) + + return c.json(publicUser(user)) + }) + + app.post('/api/auth/logout', (c) => { + const token = getCookie(c, SESSION_COOKIE) + + if (token) { + deleteSession(token) + } + + deleteCookie(c, SESSION_COOKIE, { + path: '/' + }) + + return c.json({ + ok: true + }) + }) + + app.get('/api/users/me', (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + + return c.json(publicUser(user)) + }) + app.get('/api/events', (c) => { + const user = currentUser(c) const result = listEvents({ q: c.req.query('q'), category: c.req.query('category'), cursor: c.req.query('cursor'), limit: c.req.query('limit') - }) + }, user?.id) return c.json(result.body, result.status) }) app.get('/api/events/:slug', (c) => { - const event = findEvent(c.req.param('slug')) + const user = currentUser(c) + const event = findEvent(c.req.param('slug'), user?.id) if (!event) { return c.json(null, HTTP_NOT_FOUND) @@ -34,6 +117,14 @@ export function api() { }) app.post('/api/events', async (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + let body try { @@ -46,13 +137,14 @@ export function api() { }, HTTP_BAD_REQUEST) } - const result = createEvent(body) + const result = createEvent(body, user) return c.json(result.body, result.status) }) app.post('/api/events/:id/rsvp', (c) => { - const event = rsvpEvent(c.req.param('id')) + const user = currentUser(c) + const event = rsvpEvent(c.req.param('id'), user?.id) if (!event) { return c.json(null, HTTP_NOT_FOUND) diff --git a/examples/event-board/preact-nano_kit-intl-ssr/api/users.js b/examples/event-board/preact-nano_kit-intl-ssr/api/users.js new file mode 100644 index 00000000..4b7c6610 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/api/users.js @@ -0,0 +1,87 @@ +/** + * Demo users for the mock API. Credentials are shown on the login form. + */ +const users = [ + { + id: 'u1', + username: 'ada', + password: 'lovelace', + name: 'Ada Lovelace' + }, + { + id: 'u2', + username: 'grace', + password: 'hopper', + name: 'Grace Hopper' + } +] +/** + * In-memory session storage: token -> user id. + * Sessions reset on server restart, same as the rest of the mock data. + */ +const sessions = new Map() + +/** + * Find a user by credentials. + * @param {string} username + * @param {string} password + * @returns {object | null} Matched user or `null` when credentials are invalid. + */ +export function authenticateUser(username, password) { + if (typeof username !== 'string' || typeof password !== 'string') { + return null + } + + return users.find( + user => user.username === username && user.password === password + ) || null +} + +/** + * Create a session for a user. + * @param {string} userId + * @returns {string} Session token. + */ +export function createSession(userId) { + const token = crypto.randomUUID() + + sessions.set(token, userId) + + return token +} + +/** + * Delete a session by token. + * @param {string} token + */ +export function deleteSession(token) { + sessions.delete(token) +} + +/** + * Find a user by session token. + * @param {string | undefined} token + * @returns {object | null} Session user or `null` when the session is invalid. + */ +export function findUserBySession(token) { + const userId = token && sessions.get(token) + + if (!userId) { + return null + } + + return users.find(user => user.id === userId) || null +} + +/** + * Strip private fields from a user. + * @param {object} user + * @returns {{ id: string, username: string, name: string }} Public user payload. + */ +export function publicUser(user) { + return { + id: user.id, + username: user.username, + name: user.name + } +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/app.css b/examples/event-board/preact-nano_kit-intl-ssr/src/app.css index 443ec571..edeba2e6 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/app.css +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/app.css @@ -60,6 +60,29 @@ a { font-weight: 700; } +.header__user { + display: inline-flex; + align-items: center; + gap: 16px; + font-size: 15px; + font-weight: 700; +} + +.button_link { + border: 0; + padding: 0; + background: none; + color: #0b6b4f; + cursor: pointer; + font: inherit; + font-size: 15px; + font-weight: 700; +} + +.button_link:hover { + text-decoration: underline; +} + .main { flex: 1; padding: 48px 0; diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/index.tsx b/examples/event-board/preact-nano_kit-intl-ssr/src/index.tsx index 47abadfb..fec7e28a 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/index.tsx +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/index.tsx @@ -1,6 +1,7 @@ import { layout, loadable, + notFound, page } from '@nano_kit/preact-router' import { routes } from './stores/router' @@ -18,7 +19,9 @@ export { routes } export const pages = [ layout(Layout, [ page('home', loadable(() => import('./ui/pages/Home'))), + page('login', loadable(() => import('./ui/pages/Login'))), page('newEvent', loadable(() => import('./ui/pages/NewEvent'))), - page('event', loadable(() => import('./ui/pages/Event'))) + page('event', loadable(() => import('./ui/pages/Event'))), + notFound(loadable(() => import('./ui/pages/NotFound'))) ]) ] diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/server.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/server.ts index 270ee020..d2b72114 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/server.ts +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/server.ts @@ -27,6 +27,14 @@ app.get('*', async (c) => { acceptLanguage: c.req.header('Accept-Language') }) + if (result.setCookieHeaders) { + for (const cookie of result.setCookieHeaders) { + c.header('Set-Cookie', cookie, { + append: true + }) + } + } + if (result.redirect) { return c.redirect(result.redirect, result.statusCode as RedirectStatusCode) } diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.client.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.client.ts new file mode 100644 index 00000000..d1cc0039 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.client.ts @@ -0,0 +1,22 @@ +import { Injectable$ } from '@nano_kit/store' +import type { ApiService$ } from './api' + +/** + * Browser API transport. Requests stay relative to the current origin, so + * the browser attaches cookies and `Accept-Language` on its own. + */ +export class ClientApiService$ extends Injectable$ implements ApiService$ { + fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.server.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.server.ts new file mode 100644 index 00000000..2675e09b --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.server.ts @@ -0,0 +1,62 @@ +import { + CookieStore$, + serializeCookies +} from '@nano_kit/platform-web' +import { + Injectable$, + inject +} from '@nano_kit/store' +import { IntlService$ } from '../intl' +import type { ApiService$ } from './api' + +/** + * Absolute API origin used by the server renderer. + * Browser requests stay relative to the current origin. + */ +const API_ORIGIN = import.meta.env.VITE_EVENT_BOARD_API_ORIGIN + || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') + +/** + * SSR API transport. Server-to-server requests carry no browser context, so + * the incoming request cookies and locale are forwarded explicitly. + */ +export class ServerApiService$ extends Injectable$ implements ApiService$ { + cookieStore = inject(CookieStore$) + intlService = inject(IntlService$) + + private async getHeaders() { + const headers: Record = {} + const [ + cookie, + locale + ] = await Promise.all([ + serializeCookies(this.cookieStore, ['session']), + this.intlService.getServerLocale() + ]) + + if (cookie) { + headers['Cookie'] = cookie + } + + if (locale) { + headers['Accept-Language'] = locale + } + + return headers + } + + async fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`${API_ORIGIN}/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...await this.getHeaders(), + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.ts new file mode 100644 index 00000000..59fffb3a --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.ts @@ -0,0 +1,17 @@ +import { ClientApiService$ } from './api.client' +import { ServerApiService$ } from './api.server' + +export interface ApiService$ { + /** + * Fetch wrapper that adds the API base URL, `Content-Type` and forwarded + * request headers + * @param url + * @param options + * @returns Fetch response + */ + fetch(url: string, options?: RequestInit): Promise +} + +export const ApiService$ = import.meta.env.SSR + ? ServerApiService$ + : ClientApiService$ diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.types.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.types.ts new file mode 100644 index 00000000..759840fb --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/api.types.ts @@ -0,0 +1,5 @@ +export const HttpStatus = { + BadRequest: 400, + Unauthorized: 401, + NotFound: 404 +} as const diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/index.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/index.ts new file mode 100644 index 00000000..f133e643 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/api/index.ts @@ -0,0 +1,2 @@ +export * from './api' +export * from './api.types' diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.ts index 0b5aebfd..f359daf4 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.ts +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.ts @@ -1,107 +1,107 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' import type { BoardEvent, EventsFilter, EventsPage, NewEventForm } from './events.types' +import { + ApiService$, + HttpStatus +} from './api' export * from './events.types' -/** - * Absolute API origin used by the server renderer. - * Browser requests stay relative to the current origin. - */ -const API_ORIGIN = import.meta.env.SSR - ? import.meta.env.VITE_EVENT_BOARD_API_ORIGIN || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') - : '' -const HTTP_NOT_FOUND = 404 - -/** - * Fetch a cursor-paginated page of events. - * @param filter - Search, category, cursor, and limit filters. - * @returns Events page with optional next cursor. - */ -export async function fetchEvents(filter: EventsFilter = {}) { - const params = new URLSearchParams() - - if (filter.q) { - params.set('q', filter.q) - } +export class EventsService$ extends Injectable$ { + api = inject(ApiService$) - if (filter.category) { - params.set('category', filter.category) - } + /** + * Fetch a cursor-paginated page of events. + * @param filter - Search, category, cursor, and limit filters. + * @returns Events page with optional next cursor. + */ + async fetchEvents(filter: EventsFilter = {}) { + const params = new URLSearchParams() - if (filter.cursor) { - params.set('cursor', String(filter.cursor)) - } + if (filter.q) { + params.set('q', filter.q) + } - if (filter.limit) { - params.set('limit', String(filter.limit)) - } + if (filter.category) { + params.set('category', filter.category) + } - const response = await fetch(`${API_ORIGIN}/api/events?${params}`) + if (filter.cursor) { + params.set('cursor', String(filter.cursor)) + } - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + if (filter.limit) { + params.set('limit', String(filter.limit)) + } - return await response.json() as EventsPage -} + const response = await this.api.fetch(`events?${params}`) -/** - * Fetch a single event by slug. - * @param slug - Event slug from the route. - * @returns Event details, or `null` when the event does not exist. - */ -export async function fetchEvent(slug: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${slug}`) + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } - if (response.status === HTTP_NOT_FOUND) { - return null + return await response.json() as EventsPage } - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + /** + * Fetch a single event by slug. + * @param slug - Event slug from the route. + * @returns Event details, or `null` when the event does not exist. + */ + async fetchEvent(slug: string) { + const response = await this.api.fetch(`events/${slug}`) - return await response.json() as BoardEvent -} + if (response.status === HttpStatus.NotFound) { + return null + } -/** - * Create a new event in the mock API. - * @param event - New event form payload. - * @returns Created event with generated id, slug, and attendees count. - */ -export async function createEvent(event: NewEventForm) { - const response = await fetch(`${API_ORIGIN}/api/events`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(event) - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } - return await response.json() as BoardEvent -} + return await response.json() as BoardEvent + } -/** - * Mark the current user as going to an event. - * @param id - Event id. - * @returns Updated event with incremented attendees count. - */ -export async function rsvpEvent(id: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${id}/rsvp`, { - method: 'POST' - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) + /** + * Create a new event in the mock API. + * @param event - New event form payload. + * @returns Created event with generated id, slug, and attendees count. + */ + async createEvent(event: NewEventForm) { + const response = await this.api.fetch('events', { + method: 'POST', + body: JSON.stringify(event) + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent } - return await response.json() as BoardEvent + /** + * Mark the current user as going to an event. + * @param id - Event id. + * @returns Updated event with incremented attendees count. + */ + async rsvpEvent(id: string) { + const response = await this.api.fetch(`events/${id}/rsvp`, { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent + } } diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.types.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.types.ts index ce3f3fa3..1d54d127 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.types.ts +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/events.types.ts @@ -15,7 +15,9 @@ export interface BoardEvent { startsAt: number location: string category: EventCategory + author?: string attendees: number + going: boolean } export interface EventsPage { diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/index.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/index.ts new file mode 100644 index 00000000..822c6dad --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/index.ts @@ -0,0 +1,2 @@ +export * from './intl' +export * from './translations' diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/intl.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/intl.ts new file mode 100644 index 00000000..399c35c4 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/intl.ts @@ -0,0 +1,28 @@ +import { + CookieStore$, + Locales$, + browserLocale +} from '@nano_kit/platform-web' +import { + Injectable$, + inject +} from '@nano_kit/store' +import { + type SupportedLocale, + supportedLocales +} from './translations' + +export class IntlService$ extends Injectable$ { + locales = inject(Locales$) + cookieStore = inject(CookieStore$) + + getBrowserLocale() { + return browserLocale(this.locales, supportedLocales, 'en') + } + + async getServerLocale() { + const locale = (await this.cookieStore.get('locale'))?.value + + return (locale || this.getBrowserLocale()) as SupportedLocale + } +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/translations/en.json b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/en.json similarity index 73% rename from examples/event-board/preact-nano_kit-intl-ssr/src/translations/en.json rename to examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/en.json index 0699c43d..abc9963a 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/translations/en.json +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/en.json @@ -4,7 +4,24 @@ "events": "Events", "newEvent": "New event", "language": "Language", - "loading": "Loading..." + "loading": "Loading...", + "login": "Log in", + "logout": "Log out" + }, + "login": { + "pageTitle": "Log in | Event Board", + "eyebrow": "Account", + "title": "Log in", + "description": "Sign in to create events and RSVP with your name.", + "usernameLabel": "Username", + "passwordLabel": "Password", + "demoHint": "Demo users: ada / lovelace or grace / hopper.", + "submitting": "Logging in...", + "submit": "Log in", + "errors": { + "LOGIN_INVALID_CREDENTIALS": "Invalid username or password.", + "LOGIN_FAILED": "Login failed. Please try again." + } }, "home": { "pageTitle": "Upcoming events | Event Board", @@ -49,7 +66,9 @@ "where": "Where", "going": "Going", "saving": "Saving...", - "rsvp": "I'm going" + "rsvp": "I'm going", + "rsvpCancel": "I'm not going", + "hostedBy": "Hosted by" }, "newEvent": { "pageTitle": "New event | Event Board", @@ -79,5 +98,12 @@ }, "creating": "Creating...", "create": "Create event" + }, + "notFound": { + "pageTitle": "Page not found | Event Board", + "eyebrow": "Not found", + "title": "Page not found", + "description": "This page does not exist or was moved.", + "backToEvents": "Back to events" } } diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/translations/index.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/index.ts similarity index 100% rename from examples/event-board/preact-nano_kit-intl-ssr/src/translations/index.ts rename to examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/index.ts diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/translations/ru.json b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/ru.json similarity index 73% rename from examples/event-board/preact-nano_kit-intl-ssr/src/translations/ru.json rename to examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/ru.json index 42b8e7e8..b9a2167a 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/translations/ru.json +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/intl/translations/ru.json @@ -4,7 +4,24 @@ "events": "События", "newEvent": "Новое событие", "language": "Язык", - "loading": "Загрузка..." + "loading": "Загрузка...", + "login": "Войти", + "logout": "Выйти" + }, + "login": { + "pageTitle": "Вход | Доска Событий", + "eyebrow": "Аккаунт", + "title": "Вход", + "description": "Войдите, чтобы создавать события и отмечать участие под своим именем.", + "usernameLabel": "Имя пользователя", + "passwordLabel": "Пароль", + "demoHint": "Демо-пользователи: ada / lovelace или grace / hopper.", + "submitting": "Входим...", + "submit": "Войти", + "errors": { + "LOGIN_INVALID_CREDENTIALS": "Неверное имя пользователя или пароль.", + "LOGIN_FAILED": "Не удалось войти. Попробуйте ещё раз." + } }, "home": { "pageTitle": "События | Доска Событий", @@ -51,7 +68,9 @@ "where": "Где", "going": "Участников", "saving": "Сохранение...", - "rsvp": "Я пойду" + "rsvp": "Я пойду", + "rsvpCancel": "Я не пойду", + "hostedBy": "Организатор" }, "newEvent": { "pageTitle": "Новое событие | Доска Событий", @@ -81,5 +100,12 @@ }, "creating": "Создание...", "create": "Создать событие" + }, + "notFound": { + "pageTitle": "Страница не найдена | Доска Событий", + "eyebrow": "Не найдено", + "title": "Страница не найдена", + "description": "Такой страницы не существует или она была перемещена.", + "backToEvents": "Назад к событиям" } } diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/user.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/user.ts new file mode 100644 index 00000000..2a4c7960 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/user.ts @@ -0,0 +1,73 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' +import type { + LoginParams, + User +} from './user.types' +import { + ApiService$, + HttpStatus +} from './api' +import { UserError } from './user.types' + +export * from './user.types' + +export class UserService$ extends Injectable$ { + api = inject(ApiService$) + + /** + * Fetch the current session user. + * @returns Public user payload. + */ + async getUser() { + const response = await this.api.fetch('users/me') + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.Unauthenticated) + } + + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as User + } + + /** + * Login with demo credentials. The API responds with a `Set-Cookie` header, + * so the session cookie is stored by the browser (or by the SSR cookie store). + * @param params - Username and password. + * @returns Public user payload. + */ + async login(params: LoginParams) { + const response = await this.api.fetch('auth/login', { + method: 'POST', + body: JSON.stringify(params) + }) + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.LoginInvalidCredentials) + } + + throw new Error(UserError.LoginFailed) + } + + return await response.json() as User + } + + /** + * Logout the current session. The API deletes the session cookie. + */ + async logout() { + const response = await this.api.fetch('auth/logout', { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + } +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/services/user.types.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/services/user.types.ts new file mode 100644 index 00000000..ae9e3454 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/services/user.types.ts @@ -0,0 +1,18 @@ +export const UserError = { + Unauthenticated: 'UNAUTHENTICATED', + LoginFailed: 'LOGIN_FAILED', + LoginInvalidCredentials: 'LOGIN_INVALID_CREDENTIALS' +} as const + +export type UserError = typeof UserError[keyof typeof UserError] + +export interface User { + id: string + username: string + name: string +} + +export interface LoginParams { + username: string + password: string +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/stores/events.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/events.ts index 183fc862..cf6685fb 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/stores/events.ts +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/events.ts @@ -25,13 +25,11 @@ import { type EventCategory, type EventsPage, type NewEventForm, - createEvent, - fetchEvent, - fetchEvents, - rsvpEvent + EventsService$ } from '#src/services/events' import { Params$ } from './router' import { Client$ } from './query' +import { User$ } from './user' import { datetimeLocalValue } from './utils' const SEARCH_DEBOUNCE = 600 @@ -49,6 +47,7 @@ export const EventKey = queryKey<[slug: string | undefined], BoardEvent | null>( export const EventEntity = entity('event') export function EventsList$() { + const eventsService = inject(EventsService$) const { $data, infinite @@ -76,7 +75,7 @@ export function EventsList$() { }) }) - return fetchEvents({ + return eventsService.fetchEvents({ cursor, q, category @@ -102,6 +101,7 @@ export function EventsList$() { } export function EventDetails$() { + const eventsService = inject(EventsService$) const { query } = inject(Client$) const { $slug } = inject(Params$) const [ @@ -113,7 +113,7 @@ export function EventDetails$() { [$slug], async slug => ( slug - ? await fetchEvent(slug) + ? await eventsService.fetchEvent(slug) : null ), [entities(EventEntity)] @@ -127,6 +127,7 @@ export function EventDetails$() { } export function NewEventForm$() { + const eventsService = inject(EventsService$) const { mutation, revalidate @@ -198,7 +199,7 @@ export function NewEventForm$() { })) }) - return createEvent(payload) + return eventsService.createEvent(payload) }, [ disabled(not($valid)), @@ -224,6 +225,8 @@ export function NewEventForm$() { } export function RsvpEvent$() { + const eventsService = inject(EventsService$) + const { $user } = inject(User$) const { $data, mutation @@ -235,21 +238,15 @@ export function RsvpEvent$() { $rsvpLoading ] = mutation<[id: string], BoardEvent>( (id, ctx) => { - const key = EventEntity(id) - const previousEvent = $data(key) - - if (previousEvent) { - $data(key, { - ...previousEvent, - attendees: previousEvent.attendees + 1 - }) + const revert = $data(EventEntity(id), event => event && { + ...event, + going: Boolean($user()) && !event.going, + attendees: event.attendees + (event.going ? -1 : 1) + }) - onError(ctx, () => { - $data(key, previousEvent) - }) - } + onError(ctx, revert) - return rsvpEvent(id) + return eventsService.rsvpEvent(id) } ) diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/stores/intl.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/intl.ts index e4c50a22..3048e119 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/stores/intl.ts +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/intl.ts @@ -5,29 +5,28 @@ import { intl } from '@nano_kit/intl' import { - Locales$, CookieStore$, - browserLocale, cookieStored } from '@nano_kit/platform-web' import { type SupportedLocale, + IntlService$, load, supportedLocales -} from '../translations' +} from '../services/intl' import { Client$ } from './query' -const TranslationsKey = queryKey<[locale: SupportedLocale, namespace: string], AnyTranslationData>('translations') +export const TranslationsKey = queryKey<[locale: SupportedLocale, namespace: string], AnyTranslationData>('translations') export function Intl$() { - const locales = inject(Locales$) + const intlService = inject(IntlService$) const cookieStore = inject(CookieStore$) const { query } = inject(Client$) const $locale = cookieStored(cookieStore, { name: 'locale', path: '/', sameSite: 'lax' - }, browserLocale(locales, supportedLocales, 'en')) + }, intlService.getBrowserLocale()) const { messages, $loading diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/stores/router.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/router.ts index a5cb81cf..fa718013 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/stores/router.ts +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/router.ts @@ -12,10 +12,17 @@ import { export const routes = { home: '/', + login: '/login', newEvent: '/events/new', event: '/events/:slug' } as const +export const publicRoutes = new Set([ + 'home', + 'login', + 'event' +]) + export function Params$() { const $location = inject(Location$) const $searchParams = searchParams($location) diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/stores/user.ts b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/user.ts new file mode 100644 index 00000000..87dad0f2 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/stores/user.ts @@ -0,0 +1,80 @@ +import { + inject, + onMountEffect +} from '@nano_kit/store' +import { + keys, + onSuccess, + queryKey +} from '@nano_kit/query' +import { + LocationNavigation$, + Paths$ +} from '@nano_kit/router' +import { + type LoginParams, + type User, + UserError, + UserService$ +} from '#src/services/user' +import { Client$ } from './query' +import { TranslationsKey } from './intl' +import { publicRoutes } from './router' + +export const UserKey = queryKey<[], User>('user') + +export function User$() { + const userService = inject(UserService$) + const [$location, navigation] = inject(LocationNavigation$) + const paths = inject(Paths$) + const { + query, + mutation, + invalidate + } = inject(Client$) + const [ + $user, + $userError, + $userLoading + ] = query(UserKey, [], () => userService.getUser()) + const invalidateSession = () => { + keys(Key => Key !== TranslationsKey && invalidate(Key)) + } + const [ + login, , + $loginError, + $loginLoading + ] = mutation<[params: LoginParams], User>( + (params, ctx) => { + onSuccess(ctx, () => { + invalidateSession() + navigation.replace(paths.home) + }) + + return userService.login(params) + } + ) + const logout = async () => { + await userService.logout() + invalidateSession() + } + + onMountEffect($user, () => { + const route = $location.$route() + const userError = $userError() + + if (userError === UserError.Unauthenticated && route && !publicRoutes.has(route)) { + navigation.replace(paths.login) + } + }) + + return { + login, + logout, + $user, + $userError, + $userLoading, + $loginError, + $loginLoading + } +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Event.tsx b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Event.tsx index 6076382c..5c3b8c55 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Event.tsx +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Event.tsx @@ -39,6 +39,8 @@ function Messages$() { notFoundDescription: params({ slug: text() }), + rsvpCancel: text(), + hostedBy: text(), backToEvents: text(), categories: raw>(), when: text(), @@ -167,6 +169,12 @@ export default function EventPage() {
{t.where}
{event.location}
+ {event.author && ( +
+
{t.hostedBy}
+
{event.author}
+
+ )}
{t.going}
{t.attendees(event.attendees)}
@@ -185,7 +193,7 @@ export default function EventPage() { disabled={rsvpLoading} onClick={onRsvp} > - {rsvpLoading ? t.saving : t.rsvp} + {rsvpLoading ? t.saving : event.going ? t.rsvpCancel : t.rsvp}
diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Layout.tsx b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Layout.tsx index f0bac0c6..84fa5b9f 100644 --- a/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Layout.tsx +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Layout.tsx @@ -15,6 +15,7 @@ import { } from '@nano_kit/preact-router' import { text } from '@nano_kit/intl' import { Intl$ } from '#src/stores/intl' +import { User$ } from '#src/stores/user' function Messages$() { const { messages } = inject(Intl$) @@ -23,6 +24,8 @@ function Messages$() { title: text(), events: text(), newEvent: text(), + login: text(), + logout: text(), language: text(), loading: text() }) @@ -30,8 +33,9 @@ function Messages$() { export function Stores$() { const [$t] = inject(Messages$) + const { $user } = inject(User$) - return [$t] + return [$t, $user] } export function Head$() { @@ -56,10 +60,18 @@ export default function Layout() { $locale, supportedLocales } = useInject(Intl$) + const { + $user, + logout + } = useInject(User$) const [$t, $messagesPending] = useInject(Messages$) const locale = useSignal($locale) const messagesPending = useSignal($messagesPending) + const user = useSignal($user) const t = useSignal($t) + const onLogout = () => { + void logout() + } useSyncHead() useLinkComponentPreload(true) @@ -80,6 +92,19 @@ export default function Layout() { {t.events} {t.newEvent} + +
+ {user + ? ( + <> + {user.name} + + + ) + : {t.login}} +
diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Login.tsx b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Login.tsx new file mode 100644 index 00000000..e19f5d56 --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/Login.tsx @@ -0,0 +1,131 @@ +import type { TargetedEvent } from 'preact' +import { + useInject, + useSignal +} from '@nano_kit/preact' +import { title } from '@nano_kit/preact-router' +import { inject } from '@nano_kit/store' +import { + match, + other, + text +} from '@nano_kit/intl' +import { UserError } from '#src/services/user' +import { User$ } from '#src/stores/user' +import { Intl$ } from '#src/stores/intl' + +function Messages$() { + const { messages } = inject(Intl$) + + return messages('login', { + pageTitle: text(), + eyebrow: text(), + title: text(), + description: text(), + usernameLabel: text(), + passwordLabel: text(), + demoHint: text(), + submitting: text(), + submit: text(), + errors: match('type', other(UserError.LoginFailed)) + }) +} + +export function Stores$() { + const [$t] = inject(Messages$) + + return [$t] +} + +export function Head$() { + const [$t] = inject(Messages$) + + return [ + title($t.$pageTitle) + ] +} + +export default function LoginPage() { + const { + login, + $loginError, + $loginLoading + } = useInject(User$) + const [$t, $messagesPending] = useInject(Messages$) + const t = useSignal($t) + const messagesPending = useSignal($messagesPending) + const error = useSignal($loginError) + const loading = useSignal($loginLoading) + const onSubmit = (event: TargetedEvent) => { + event.preventDefault() + + const formData = new FormData(event.currentTarget) + const username = formData.get('username') + const password = formData.get('password') + + void login({ + username: typeof username === 'string' ? username : '', + password: typeof password === 'string' ? password : '' + }) + } + + return ( +
+
+

{t.eyebrow}

+

{t.title}

+

+ {t.description} +

+

+ {t.demoHint} +

+
+ +
+ + + + + {error && ( +
+ {t.errors(error)} +
+ )} + + +
+ + {messagesPending && ( +
+
+
+
+ )} +
+ ) +} diff --git a/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/NotFound.tsx b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/NotFound.tsx new file mode 100644 index 00000000..ab499d0f --- /dev/null +++ b/examples/event-board/preact-nano_kit-intl-ssr/src/ui/pages/NotFound.tsx @@ -0,0 +1,70 @@ +import { + useInject, + useSignal +} from '@nano_kit/preact' +import { + Link, + title +} from '@nano_kit/preact-router' +import { inject } from '@nano_kit/store' +import { text } from '@nano_kit/intl' +import { Intl$ } from '#src/stores/intl' + +function Messages$() { + const { messages } = inject(Intl$) + + return messages('notFound', { + pageTitle: text(), + eyebrow: text(), + title: text(), + description: text(), + backToEvents: text() + }) +} + +export function Stores$() { + const [$t] = inject(Messages$) + + return [$t] +} + +export function Head$() { + const [$t] = inject(Messages$) + + return [ + title($t.$pageTitle) + ] +} + +export default function NotFoundPage() { + const [$t, $messagesPending] = useInject(Messages$) + const t = useSignal($t) + const messagesPending = useSignal($messagesPending) + + return ( +
+
+

{t.eyebrow}

+

{t.title}

+

+ {t.description} +

+
+ + + {t.backToEvents} + + + {messagesPending && ( +
+
+
+
+ )} +
+ ) +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/.env.example b/examples/event-board/react-nano_kit-intl-ssr/.env.example new file mode 100644 index 00000000..bfd7e450 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/.env.example @@ -0,0 +1,6 @@ +# Absolute API origin for SSR fetches. +# Defaults: http://localhost:5173 in dev, http://localhost:3001 in production. +VITE_EVENT_BOARD_API_ORIGIN= + +# Production server port (default 3001). +PORT=3001 diff --git a/examples/event-board/react-nano_kit-intl-ssr/api/events.js b/examples/event-board/react-nano_kit-intl-ssr/api/events.js index c14e7bf1..560477d3 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/api/events.js +++ b/examples/event-board/react-nano_kit-intl-ssr/api/events.js @@ -19,7 +19,8 @@ const events = [ startsAt: new Date('2026-05-12T18:00:00Z').getTime(), location: 'Online', category: 'workshop', - attendees: 24 + guests: 24, + attendeeIds: [] }, { id: '2', @@ -29,7 +30,8 @@ const events = [ startsAt: new Date('2026-05-21T19:30:00Z').getTime(), location: 'Berlin', category: 'meetup', - attendees: 58 + guests: 58, + attendeeIds: [] }, { id: '3', @@ -39,7 +41,8 @@ const events = [ startsAt: new Date('2026-06-03T17:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 102 + guests: 102, + attendeeIds: [] }, { id: '4', @@ -49,7 +52,8 @@ const events = [ startsAt: new Date('2026-06-11T18:30:00Z').getTime(), location: 'Prague', category: 'meetup', - attendees: 41 + guests: 41, + attendeeIds: [] }, { id: '5', @@ -59,7 +63,8 @@ const events = [ startsAt: new Date('2026-06-24T09:00:00Z').getTime(), location: 'Amsterdam', category: 'conference', - attendees: 180 + guests: 180, + attendeeIds: [] }, { id: '6', @@ -69,7 +74,8 @@ const events = [ startsAt: new Date('2026-07-02T16:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 76 + guests: 76, + attendeeIds: [] } ] @@ -116,6 +122,26 @@ function sortEvents(items) { return [...items].sort((a, b) => a.startsAt - b.startsAt || a.id.localeCompare(b.id)) } +/** + * Convert an internal event record into an API payload for a user. + * @param {object} event - Internal event record. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object} Event payload with `attendees` count and personal `going` flag. + */ +function serializeEvent(event, userId) { + const { + guests, + attendeeIds, + ...publicEvent + } = event + + return { + ...publicEvent, + attendees: guests + attendeeIds.length, + going: userId ? attendeeIds.includes(userId) : false + } +} + function validateEventInput(input) { const errors = {} @@ -155,9 +181,10 @@ function validateEventInput(input) { * @param {string | undefined} query.category - Category filter. * @param {string | undefined} query.cursor - Anchor cursor based on `startsAt`. * @param {string | undefined} query.limit - Page size. + * @param {string | undefined} userId - Current user id, if authenticated. * @returns {{ status: number, body: { events: object[], nextCursor?: number } | { error: string } }} API response payload. */ -export function listEvents(query) { +export function listEvents(query, userId) { const q = query.q?.trim().toLowerCase() const category = normalizeCategory(query.category) const cursor = Number(query.cursor || 0) @@ -193,7 +220,7 @@ export function listEvents(query) { return { status: 200, body: { - events: pageItems, + events: pageItems.map(event => serializeEvent(event, userId)), nextCursor: hasMore ? pageItems[pageItems.length - 1]?.startsAt : undefined } } @@ -202,18 +229,22 @@ export function listEvents(query) { /** * Find an event by slug. * @param {string} slug - Event slug. - * @returns {object | null} Event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Event payload or `null` when it does not exist. */ -export function findEvent(slug) { - return events.find(event => event.slug === slug) || null +export function findEvent(slug, userId) { + const event = events.find(item => item.slug === slug) || null + + return event && serializeEvent(event, userId) } /** * Create a new event in the in-memory store. * @param {object} input - Event form payload. + * @param {object | null} user - Current user, if authenticated. * @returns {{ status: number, body: object }} API response payload. */ -export function createEvent(input) { +export function createEvent(input, user) { const errors = validateEventInput(input) if (Object.keys(errors).length > 0) { @@ -233,30 +264,42 @@ export function createEvent(input) { startsAt: input.startsAt, location: input.location.trim(), category: input.category, - attendees: 0 + author: user?.name, + guests: 0, + attendeeIds: [] } events.push(event) return { status: 201, - body: event + body: serializeEvent(event, user?.id) } } /** - * Increment attendees count for an event. + * Register an RSVP for an event. An authenticated user toggles their personal + * attendance, an anonymous request just increments the guest counter. * @param {string} id - Event id. - * @returns {object | null} Updated event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Updated event payload or `null` when it does not exist. */ -export function rsvpEvent(id) { +export function rsvpEvent(id, userId) { const event = events.find(item => item.id === id) || null if (!event) { return null } - event.attendees += 1 + if (userId) { + if (event.attendeeIds.includes(userId)) { + event.attendeeIds = event.attendeeIds.filter(attendeeId => attendeeId !== userId) + } else { + event.attendeeIds = [...event.attendeeIds, userId] + } + } else { + event.guests += 1 + } - return event + return serializeEvent(event, userId) } diff --git a/examples/event-board/react-nano_kit-intl-ssr/api/index.js b/examples/event-board/react-nano_kit-intl-ssr/api/index.js index 738b5dcc..51ad77c9 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/api/index.js +++ b/examples/event-board/react-nano_kit-intl-ssr/api/index.js @@ -1,30 +1,113 @@ import { Hono } from 'hono' +import { + deleteCookie, + getCookie, + setCookie +} from 'hono/cookie' import { createEvent, findEvent, listEvents, rsvpEvent } from './events.js' +import { + authenticateUser, + createSession, + deleteSession, + findUserBySession, + publicUser +} from './users.js' const HTTP_BAD_REQUEST = 400 +const HTTP_UNAUTHORIZED = 401 const HTTP_NOT_FOUND = 404 +const SESSION_COOKIE = 'session' +const SESSION_MAX_AGE = 60 * 60 * 24 * 30 + +/** + * Resolve the current user from the request session cookie. + * @param {import('hono').Context} c + * @returns {object | null} Session user or `null`. + */ +function currentUser(c) { + return findUserBySession(getCookie(c, SESSION_COOKIE)) +} export function api() { const app = new Hono() + app.post('/api/auth/login', async (c) => { + let body + + try { + body = await c.req.json() + } catch { + return c.json({ + error: 'Expected JSON payload' + }, HTTP_BAD_REQUEST) + } + + const user = authenticateUser(body?.username, body?.password) + + if (!user) { + return c.json({ + error: 'Invalid username or password' + }, HTTP_UNAUTHORIZED) + } + + setCookie(c, SESSION_COOKIE, createSession(user.id), { + path: '/', + httpOnly: true, + sameSite: 'Lax', + maxAge: SESSION_MAX_AGE + }) + + return c.json(publicUser(user)) + }) + + app.post('/api/auth/logout', (c) => { + const token = getCookie(c, SESSION_COOKIE) + + if (token) { + deleteSession(token) + } + + deleteCookie(c, SESSION_COOKIE, { + path: '/' + }) + + return c.json({ + ok: true + }) + }) + + app.get('/api/users/me', (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + + return c.json(publicUser(user)) + }) + app.get('/api/events', (c) => { + const user = currentUser(c) const result = listEvents({ q: c.req.query('q'), category: c.req.query('category'), cursor: c.req.query('cursor'), limit: c.req.query('limit') - }) + }, user?.id) return c.json(result.body, result.status) }) app.get('/api/events/:slug', (c) => { - const event = findEvent(c.req.param('slug')) + const user = currentUser(c) + const event = findEvent(c.req.param('slug'), user?.id) if (!event) { return c.json(null, HTTP_NOT_FOUND) @@ -34,6 +117,14 @@ export function api() { }) app.post('/api/events', async (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + let body try { @@ -46,13 +137,14 @@ export function api() { }, HTTP_BAD_REQUEST) } - const result = createEvent(body) + const result = createEvent(body, user) return c.json(result.body, result.status) }) app.post('/api/events/:id/rsvp', (c) => { - const event = rsvpEvent(c.req.param('id')) + const user = currentUser(c) + const event = rsvpEvent(c.req.param('id'), user?.id) if (!event) { return c.json(null, HTTP_NOT_FOUND) diff --git a/examples/event-board/react-nano_kit-intl-ssr/api/users.js b/examples/event-board/react-nano_kit-intl-ssr/api/users.js new file mode 100644 index 00000000..4b7c6610 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/api/users.js @@ -0,0 +1,87 @@ +/** + * Demo users for the mock API. Credentials are shown on the login form. + */ +const users = [ + { + id: 'u1', + username: 'ada', + password: 'lovelace', + name: 'Ada Lovelace' + }, + { + id: 'u2', + username: 'grace', + password: 'hopper', + name: 'Grace Hopper' + } +] +/** + * In-memory session storage: token -> user id. + * Sessions reset on server restart, same as the rest of the mock data. + */ +const sessions = new Map() + +/** + * Find a user by credentials. + * @param {string} username + * @param {string} password + * @returns {object | null} Matched user or `null` when credentials are invalid. + */ +export function authenticateUser(username, password) { + if (typeof username !== 'string' || typeof password !== 'string') { + return null + } + + return users.find( + user => user.username === username && user.password === password + ) || null +} + +/** + * Create a session for a user. + * @param {string} userId + * @returns {string} Session token. + */ +export function createSession(userId) { + const token = crypto.randomUUID() + + sessions.set(token, userId) + + return token +} + +/** + * Delete a session by token. + * @param {string} token + */ +export function deleteSession(token) { + sessions.delete(token) +} + +/** + * Find a user by session token. + * @param {string | undefined} token + * @returns {object | null} Session user or `null` when the session is invalid. + */ +export function findUserBySession(token) { + const userId = token && sessions.get(token) + + if (!userId) { + return null + } + + return users.find(user => user.id === userId) || null +} + +/** + * Strip private fields from a user. + * @param {object} user + * @returns {{ id: string, username: string, name: string }} Public user payload. + */ +export function publicUser(user) { + return { + id: user.id, + username: user.username, + name: user.name + } +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/app.css b/examples/event-board/react-nano_kit-intl-ssr/src/app.css index e7c2a0ac..84ad4016 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/app.css +++ b/examples/event-board/react-nano_kit-intl-ssr/src/app.css @@ -60,6 +60,29 @@ a { font-weight: 700; } +.header__user { + display: inline-flex; + align-items: center; + gap: 16px; + font-size: 15px; + font-weight: 700; +} + +.button_link { + border: 0; + padding: 0; + background: none; + color: #0b6b4f; + cursor: pointer; + font: inherit; + font-size: 15px; + font-weight: 700; +} + +.button_link:hover { + text-decoration: underline; +} + .main { flex: 1; padding: 48px 0; diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/index.tsx b/examples/event-board/react-nano_kit-intl-ssr/src/index.tsx index e7e1d1d0..9df0e91e 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/index.tsx +++ b/examples/event-board/react-nano_kit-intl-ssr/src/index.tsx @@ -1,6 +1,7 @@ import { layout, loadable, + notFound, page } from '@nano_kit/react-router' import { routes } from './stores/router' @@ -18,7 +19,9 @@ export { routes } export const pages = [ layout(Layout, [ page('home', loadable(() => import('./ui/pages/Home'))), + page('login', loadable(() => import('./ui/pages/Login'))), page('newEvent', loadable(() => import('./ui/pages/NewEvent'))), - page('event', loadable(() => import('./ui/pages/Event'))) + page('event', loadable(() => import('./ui/pages/Event'))), + notFound(loadable(() => import('./ui/pages/NotFound'))) ]) ] diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/server.ts b/examples/event-board/react-nano_kit-intl-ssr/src/server.ts index 270ee020..d2b72114 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/server.ts +++ b/examples/event-board/react-nano_kit-intl-ssr/src/server.ts @@ -27,6 +27,14 @@ app.get('*', async (c) => { acceptLanguage: c.req.header('Accept-Language') }) + if (result.setCookieHeaders) { + for (const cookie of result.setCookieHeaders) { + c.header('Set-Cookie', cookie, { + append: true + }) + } + } + if (result.redirect) { return c.redirect(result.redirect, result.statusCode as RedirectStatusCode) } diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.client.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.client.ts new file mode 100644 index 00000000..d1cc0039 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.client.ts @@ -0,0 +1,22 @@ +import { Injectable$ } from '@nano_kit/store' +import type { ApiService$ } from './api' + +/** + * Browser API transport. Requests stay relative to the current origin, so + * the browser attaches cookies and `Accept-Language` on its own. + */ +export class ClientApiService$ extends Injectable$ implements ApiService$ { + fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.server.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.server.ts new file mode 100644 index 00000000..2675e09b --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.server.ts @@ -0,0 +1,62 @@ +import { + CookieStore$, + serializeCookies +} from '@nano_kit/platform-web' +import { + Injectable$, + inject +} from '@nano_kit/store' +import { IntlService$ } from '../intl' +import type { ApiService$ } from './api' + +/** + * Absolute API origin used by the server renderer. + * Browser requests stay relative to the current origin. + */ +const API_ORIGIN = import.meta.env.VITE_EVENT_BOARD_API_ORIGIN + || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') + +/** + * SSR API transport. Server-to-server requests carry no browser context, so + * the incoming request cookies and locale are forwarded explicitly. + */ +export class ServerApiService$ extends Injectable$ implements ApiService$ { + cookieStore = inject(CookieStore$) + intlService = inject(IntlService$) + + private async getHeaders() { + const headers: Record = {} + const [ + cookie, + locale + ] = await Promise.all([ + serializeCookies(this.cookieStore, ['session']), + this.intlService.getServerLocale() + ]) + + if (cookie) { + headers['Cookie'] = cookie + } + + if (locale) { + headers['Accept-Language'] = locale + } + + return headers + } + + async fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`${API_ORIGIN}/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...await this.getHeaders(), + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.ts new file mode 100644 index 00000000..59fffb3a --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.ts @@ -0,0 +1,17 @@ +import { ClientApiService$ } from './api.client' +import { ServerApiService$ } from './api.server' + +export interface ApiService$ { + /** + * Fetch wrapper that adds the API base URL, `Content-Type` and forwarded + * request headers + * @param url + * @param options + * @returns Fetch response + */ + fetch(url: string, options?: RequestInit): Promise +} + +export const ApiService$ = import.meta.env.SSR + ? ServerApiService$ + : ClientApiService$ diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.types.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.types.ts new file mode 100644 index 00000000..759840fb --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/api.types.ts @@ -0,0 +1,5 @@ +export const HttpStatus = { + BadRequest: 400, + Unauthorized: 401, + NotFound: 404 +} as const diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/api/index.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/index.ts new file mode 100644 index 00000000..f133e643 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/api/index.ts @@ -0,0 +1,2 @@ +export * from './api' +export * from './api.types' diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/events.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/events.ts index 0b5aebfd..f359daf4 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/services/events.ts +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/events.ts @@ -1,107 +1,107 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' import type { BoardEvent, EventsFilter, EventsPage, NewEventForm } from './events.types' +import { + ApiService$, + HttpStatus +} from './api' export * from './events.types' -/** - * Absolute API origin used by the server renderer. - * Browser requests stay relative to the current origin. - */ -const API_ORIGIN = import.meta.env.SSR - ? import.meta.env.VITE_EVENT_BOARD_API_ORIGIN || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') - : '' -const HTTP_NOT_FOUND = 404 - -/** - * Fetch a cursor-paginated page of events. - * @param filter - Search, category, cursor, and limit filters. - * @returns Events page with optional next cursor. - */ -export async function fetchEvents(filter: EventsFilter = {}) { - const params = new URLSearchParams() - - if (filter.q) { - params.set('q', filter.q) - } +export class EventsService$ extends Injectable$ { + api = inject(ApiService$) - if (filter.category) { - params.set('category', filter.category) - } + /** + * Fetch a cursor-paginated page of events. + * @param filter - Search, category, cursor, and limit filters. + * @returns Events page with optional next cursor. + */ + async fetchEvents(filter: EventsFilter = {}) { + const params = new URLSearchParams() - if (filter.cursor) { - params.set('cursor', String(filter.cursor)) - } + if (filter.q) { + params.set('q', filter.q) + } - if (filter.limit) { - params.set('limit', String(filter.limit)) - } + if (filter.category) { + params.set('category', filter.category) + } - const response = await fetch(`${API_ORIGIN}/api/events?${params}`) + if (filter.cursor) { + params.set('cursor', String(filter.cursor)) + } - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + if (filter.limit) { + params.set('limit', String(filter.limit)) + } - return await response.json() as EventsPage -} + const response = await this.api.fetch(`events?${params}`) -/** - * Fetch a single event by slug. - * @param slug - Event slug from the route. - * @returns Event details, or `null` when the event does not exist. - */ -export async function fetchEvent(slug: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${slug}`) + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } - if (response.status === HTTP_NOT_FOUND) { - return null + return await response.json() as EventsPage } - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + /** + * Fetch a single event by slug. + * @param slug - Event slug from the route. + * @returns Event details, or `null` when the event does not exist. + */ + async fetchEvent(slug: string) { + const response = await this.api.fetch(`events/${slug}`) - return await response.json() as BoardEvent -} + if (response.status === HttpStatus.NotFound) { + return null + } -/** - * Create a new event in the mock API. - * @param event - New event form payload. - * @returns Created event with generated id, slug, and attendees count. - */ -export async function createEvent(event: NewEventForm) { - const response = await fetch(`${API_ORIGIN}/api/events`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(event) - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } - return await response.json() as BoardEvent -} + return await response.json() as BoardEvent + } -/** - * Mark the current user as going to an event. - * @param id - Event id. - * @returns Updated event with incremented attendees count. - */ -export async function rsvpEvent(id: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${id}/rsvp`, { - method: 'POST' - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) + /** + * Create a new event in the mock API. + * @param event - New event form payload. + * @returns Created event with generated id, slug, and attendees count. + */ + async createEvent(event: NewEventForm) { + const response = await this.api.fetch('events', { + method: 'POST', + body: JSON.stringify(event) + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent } - return await response.json() as BoardEvent + /** + * Mark the current user as going to an event. + * @param id - Event id. + * @returns Updated event with incremented attendees count. + */ + async rsvpEvent(id: string) { + const response = await this.api.fetch(`events/${id}/rsvp`, { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent + } } diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/events.types.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/events.types.ts index ce3f3fa3..1d54d127 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/services/events.types.ts +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/events.types.ts @@ -15,7 +15,9 @@ export interface BoardEvent { startsAt: number location: string category: EventCategory + author?: string attendees: number + going: boolean } export interface EventsPage { diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/index.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/index.ts new file mode 100644 index 00000000..822c6dad --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/index.ts @@ -0,0 +1,2 @@ +export * from './intl' +export * from './translations' diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/intl.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/intl.ts new file mode 100644 index 00000000..399c35c4 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/intl.ts @@ -0,0 +1,28 @@ +import { + CookieStore$, + Locales$, + browserLocale +} from '@nano_kit/platform-web' +import { + Injectable$, + inject +} from '@nano_kit/store' +import { + type SupportedLocale, + supportedLocales +} from './translations' + +export class IntlService$ extends Injectable$ { + locales = inject(Locales$) + cookieStore = inject(CookieStore$) + + getBrowserLocale() { + return browserLocale(this.locales, supportedLocales, 'en') + } + + async getServerLocale() { + const locale = (await this.cookieStore.get('locale'))?.value + + return (locale || this.getBrowserLocale()) as SupportedLocale + } +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/translations/en.json b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/en.json similarity index 73% rename from examples/event-board/react-nano_kit-intl-ssr/src/translations/en.json rename to examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/en.json index 0699c43d..abc9963a 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/translations/en.json +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/en.json @@ -4,7 +4,24 @@ "events": "Events", "newEvent": "New event", "language": "Language", - "loading": "Loading..." + "loading": "Loading...", + "login": "Log in", + "logout": "Log out" + }, + "login": { + "pageTitle": "Log in | Event Board", + "eyebrow": "Account", + "title": "Log in", + "description": "Sign in to create events and RSVP with your name.", + "usernameLabel": "Username", + "passwordLabel": "Password", + "demoHint": "Demo users: ada / lovelace or grace / hopper.", + "submitting": "Logging in...", + "submit": "Log in", + "errors": { + "LOGIN_INVALID_CREDENTIALS": "Invalid username or password.", + "LOGIN_FAILED": "Login failed. Please try again." + } }, "home": { "pageTitle": "Upcoming events | Event Board", @@ -49,7 +66,9 @@ "where": "Where", "going": "Going", "saving": "Saving...", - "rsvp": "I'm going" + "rsvp": "I'm going", + "rsvpCancel": "I'm not going", + "hostedBy": "Hosted by" }, "newEvent": { "pageTitle": "New event | Event Board", @@ -79,5 +98,12 @@ }, "creating": "Creating...", "create": "Create event" + }, + "notFound": { + "pageTitle": "Page not found | Event Board", + "eyebrow": "Not found", + "title": "Page not found", + "description": "This page does not exist or was moved.", + "backToEvents": "Back to events" } } diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/translations/index.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/index.ts similarity index 100% rename from examples/event-board/react-nano_kit-intl-ssr/src/translations/index.ts rename to examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/index.ts diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/translations/ru.json b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/ru.json similarity index 73% rename from examples/event-board/react-nano_kit-intl-ssr/src/translations/ru.json rename to examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/ru.json index 42b8e7e8..b9a2167a 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/translations/ru.json +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/intl/translations/ru.json @@ -4,7 +4,24 @@ "events": "События", "newEvent": "Новое событие", "language": "Язык", - "loading": "Загрузка..." + "loading": "Загрузка...", + "login": "Войти", + "logout": "Выйти" + }, + "login": { + "pageTitle": "Вход | Доска Событий", + "eyebrow": "Аккаунт", + "title": "Вход", + "description": "Войдите, чтобы создавать события и отмечать участие под своим именем.", + "usernameLabel": "Имя пользователя", + "passwordLabel": "Пароль", + "demoHint": "Демо-пользователи: ada / lovelace или grace / hopper.", + "submitting": "Входим...", + "submit": "Войти", + "errors": { + "LOGIN_INVALID_CREDENTIALS": "Неверное имя пользователя или пароль.", + "LOGIN_FAILED": "Не удалось войти. Попробуйте ещё раз." + } }, "home": { "pageTitle": "События | Доска Событий", @@ -51,7 +68,9 @@ "where": "Где", "going": "Участников", "saving": "Сохранение...", - "rsvp": "Я пойду" + "rsvp": "Я пойду", + "rsvpCancel": "Я не пойду", + "hostedBy": "Организатор" }, "newEvent": { "pageTitle": "Новое событие | Доска Событий", @@ -81,5 +100,12 @@ }, "creating": "Создание...", "create": "Создать событие" + }, + "notFound": { + "pageTitle": "Страница не найдена | Доска Событий", + "eyebrow": "Не найдено", + "title": "Страница не найдена", + "description": "Такой страницы не существует или она была перемещена.", + "backToEvents": "Назад к событиям" } } diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/user.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/user.ts new file mode 100644 index 00000000..2a4c7960 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/user.ts @@ -0,0 +1,73 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' +import type { + LoginParams, + User +} from './user.types' +import { + ApiService$, + HttpStatus +} from './api' +import { UserError } from './user.types' + +export * from './user.types' + +export class UserService$ extends Injectable$ { + api = inject(ApiService$) + + /** + * Fetch the current session user. + * @returns Public user payload. + */ + async getUser() { + const response = await this.api.fetch('users/me') + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.Unauthenticated) + } + + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as User + } + + /** + * Login with demo credentials. The API responds with a `Set-Cookie` header, + * so the session cookie is stored by the browser (or by the SSR cookie store). + * @param params - Username and password. + * @returns Public user payload. + */ + async login(params: LoginParams) { + const response = await this.api.fetch('auth/login', { + method: 'POST', + body: JSON.stringify(params) + }) + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.LoginInvalidCredentials) + } + + throw new Error(UserError.LoginFailed) + } + + return await response.json() as User + } + + /** + * Logout the current session. The API deletes the session cookie. + */ + async logout() { + const response = await this.api.fetch('auth/logout', { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + } +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/services/user.types.ts b/examples/event-board/react-nano_kit-intl-ssr/src/services/user.types.ts new file mode 100644 index 00000000..ae9e3454 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/services/user.types.ts @@ -0,0 +1,18 @@ +export const UserError = { + Unauthenticated: 'UNAUTHENTICATED', + LoginFailed: 'LOGIN_FAILED', + LoginInvalidCredentials: 'LOGIN_INVALID_CREDENTIALS' +} as const + +export type UserError = typeof UserError[keyof typeof UserError] + +export interface User { + id: string + username: string + name: string +} + +export interface LoginParams { + username: string + password: string +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/stores/events.ts b/examples/event-board/react-nano_kit-intl-ssr/src/stores/events.ts index 183fc862..cf6685fb 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/stores/events.ts +++ b/examples/event-board/react-nano_kit-intl-ssr/src/stores/events.ts @@ -25,13 +25,11 @@ import { type EventCategory, type EventsPage, type NewEventForm, - createEvent, - fetchEvent, - fetchEvents, - rsvpEvent + EventsService$ } from '#src/services/events' import { Params$ } from './router' import { Client$ } from './query' +import { User$ } from './user' import { datetimeLocalValue } from './utils' const SEARCH_DEBOUNCE = 600 @@ -49,6 +47,7 @@ export const EventKey = queryKey<[slug: string | undefined], BoardEvent | null>( export const EventEntity = entity('event') export function EventsList$() { + const eventsService = inject(EventsService$) const { $data, infinite @@ -76,7 +75,7 @@ export function EventsList$() { }) }) - return fetchEvents({ + return eventsService.fetchEvents({ cursor, q, category @@ -102,6 +101,7 @@ export function EventsList$() { } export function EventDetails$() { + const eventsService = inject(EventsService$) const { query } = inject(Client$) const { $slug } = inject(Params$) const [ @@ -113,7 +113,7 @@ export function EventDetails$() { [$slug], async slug => ( slug - ? await fetchEvent(slug) + ? await eventsService.fetchEvent(slug) : null ), [entities(EventEntity)] @@ -127,6 +127,7 @@ export function EventDetails$() { } export function NewEventForm$() { + const eventsService = inject(EventsService$) const { mutation, revalidate @@ -198,7 +199,7 @@ export function NewEventForm$() { })) }) - return createEvent(payload) + return eventsService.createEvent(payload) }, [ disabled(not($valid)), @@ -224,6 +225,8 @@ export function NewEventForm$() { } export function RsvpEvent$() { + const eventsService = inject(EventsService$) + const { $user } = inject(User$) const { $data, mutation @@ -235,21 +238,15 @@ export function RsvpEvent$() { $rsvpLoading ] = mutation<[id: string], BoardEvent>( (id, ctx) => { - const key = EventEntity(id) - const previousEvent = $data(key) - - if (previousEvent) { - $data(key, { - ...previousEvent, - attendees: previousEvent.attendees + 1 - }) + const revert = $data(EventEntity(id), event => event && { + ...event, + going: Boolean($user()) && !event.going, + attendees: event.attendees + (event.going ? -1 : 1) + }) - onError(ctx, () => { - $data(key, previousEvent) - }) - } + onError(ctx, revert) - return rsvpEvent(id) + return eventsService.rsvpEvent(id) } ) diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/stores/intl.ts b/examples/event-board/react-nano_kit-intl-ssr/src/stores/intl.ts index 15811599..5092bb1a 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/stores/intl.ts +++ b/examples/event-board/react-nano_kit-intl-ssr/src/stores/intl.ts @@ -2,30 +2,29 @@ import { inject } from '@nano_kit/store' import { queryKey } from '@nano_kit/query' import { intl } from '@nano_kit/intl' import { - Locales$, CookieStore$, - browserLocale, cookieStored } from '@nano_kit/platform-web' import { type SupportedLocale, type Translations, + IntlService$, load, supportedLocales -} from '../translations' +} from '../services/intl' import { Client$ } from './query' -const TranslationsKey = queryKey<[locale: SupportedLocale], Translations>('translations') +export const TranslationsKey = queryKey<[locale: SupportedLocale], Translations>('translations') export function Intl$() { - const locales = inject(Locales$) + const intlService = inject(IntlService$) const cookieStore = inject(CookieStore$) const { query } = inject(Client$) const $locale = cookieStored(cookieStore, { name: 'locale', path: '/', sameSite: 'lax' - }, browserLocale(locales, supportedLocales, 'en')) + }, intlService.getBrowserLocale()) const { messages, $loading diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/stores/router.ts b/examples/event-board/react-nano_kit-intl-ssr/src/stores/router.ts index a5cb81cf..fa718013 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/stores/router.ts +++ b/examples/event-board/react-nano_kit-intl-ssr/src/stores/router.ts @@ -12,10 +12,17 @@ import { export const routes = { home: '/', + login: '/login', newEvent: '/events/new', event: '/events/:slug' } as const +export const publicRoutes = new Set([ + 'home', + 'login', + 'event' +]) + export function Params$() { const $location = inject(Location$) const $searchParams = searchParams($location) diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/stores/user.ts b/examples/event-board/react-nano_kit-intl-ssr/src/stores/user.ts new file mode 100644 index 00000000..87dad0f2 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/stores/user.ts @@ -0,0 +1,80 @@ +import { + inject, + onMountEffect +} from '@nano_kit/store' +import { + keys, + onSuccess, + queryKey +} from '@nano_kit/query' +import { + LocationNavigation$, + Paths$ +} from '@nano_kit/router' +import { + type LoginParams, + type User, + UserError, + UserService$ +} from '#src/services/user' +import { Client$ } from './query' +import { TranslationsKey } from './intl' +import { publicRoutes } from './router' + +export const UserKey = queryKey<[], User>('user') + +export function User$() { + const userService = inject(UserService$) + const [$location, navigation] = inject(LocationNavigation$) + const paths = inject(Paths$) + const { + query, + mutation, + invalidate + } = inject(Client$) + const [ + $user, + $userError, + $userLoading + ] = query(UserKey, [], () => userService.getUser()) + const invalidateSession = () => { + keys(Key => Key !== TranslationsKey && invalidate(Key)) + } + const [ + login, , + $loginError, + $loginLoading + ] = mutation<[params: LoginParams], User>( + (params, ctx) => { + onSuccess(ctx, () => { + invalidateSession() + navigation.replace(paths.home) + }) + + return userService.login(params) + } + ) + const logout = async () => { + await userService.logout() + invalidateSession() + } + + onMountEffect($user, () => { + const route = $location.$route() + const userError = $userError() + + if (userError === UserError.Unauthenticated && route && !publicRoutes.has(route)) { + navigation.replace(paths.login) + } + }) + + return { + login, + logout, + $user, + $userError, + $userLoading, + $loginError, + $loginLoading + } +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Event.tsx b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Event.tsx index 7b821aaa..197d28ff 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Event.tsx +++ b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Event.tsx @@ -153,6 +153,12 @@ export default function EventPage() {
{t.where}
{event.location}
+ {event.author && ( +
+
{t.hostedBy}
+
{event.author}
+
+ )}
{t.going}
{t.attendees(event.attendees)}
@@ -171,7 +177,7 @@ export default function EventPage() { disabled={rsvpLoading} onClick={onRsvp} > - {rsvpLoading ? t.saving : t.rsvp} + {rsvpLoading ? t.saving : event.going ? t.rsvpCancel : t.rsvp}
diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Layout.tsx b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Layout.tsx index a9793165..116e8ba1 100644 --- a/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Layout.tsx +++ b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Layout.tsx @@ -14,6 +14,7 @@ import { useSyncHead } from '@nano_kit/react-router' import { Intl$ } from '#src/stores/intl' +import { User$ } from '#src/stores/user' function Messages$() { const { messages } = inject(Intl$) @@ -23,8 +24,9 @@ function Messages$() { export function Stores$() { const [$t] = inject(Messages$) + const { $user } = inject(User$) - return [$t] + return [$t, $user] } export function Head$() { @@ -50,10 +52,18 @@ export default function Layout() { $loading, supportedLocales } = useInject(Intl$) + const { + $user, + logout + } = useInject(User$) const [$t] = useInject(Messages$) const locale = useSignal($locale) const loading = useSignal($loading) + const user = useSignal($user) const t = useSignal($t) + const onLogout = () => { + void logout() + } useSyncHead() useLinkComponentPreload(true) @@ -71,6 +81,19 @@ export default function Layout() { {t.events} {t.newEvent} + +
+ {user + ? ( + <> + {user.name} + + + ) + : {t.login}} +
diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Login.tsx b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Login.tsx new file mode 100644 index 00000000..7c9d1719 --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/Login.tsx @@ -0,0 +1,109 @@ +import type { FormEvent } from 'react' +import { + useInject, + useSignal +} from '@nano_kit/react' +import { title } from '@nano_kit/react-router' +import { inject } from '@nano_kit/store' +import { + match, + other +} from '@nano_kit/intl' +import { UserError } from '#src/services/user' +import { User$ } from '#src/stores/user' +import { Intl$ } from '#src/stores/intl' + +function Messages$() { + const { messages } = inject(Intl$) + + return messages('login', { + errors: match('type', other(UserError.LoginFailed)) + }) +} + +export function Stores$() { + const [$t] = inject(Messages$) + + return [$t] +} + +export function Head$() { + const [$t] = inject(Messages$) + + return [ + title($t.$pageTitle) + ] +} + +export default function LoginPage() { + const { + login, + $loginError, + $loginLoading + } = useInject(User$) + const [$t] = useInject(Messages$) + const t = useSignal($t) + const error = useSignal($loginError) + const loading = useSignal($loginLoading) + const onSubmit = (event: FormEvent) => { + event.preventDefault() + + const formData = new FormData(event.currentTarget) + const username = formData.get('username') + const password = formData.get('password') + + void login({ + username: typeof username === 'string' ? username : '', + password: typeof password === 'string' ? password : '' + }) + } + + return ( +
+
+

{t.eyebrow}

+

{t.title}

+

+ {t.description} +

+

+ {t.demoHint} +

+
+ +
+ + + + + {error && ( +
+ {t.errors(error)} +
+ )} + + +
+
+ ) +} diff --git a/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/NotFound.tsx b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/NotFound.tsx new file mode 100644 index 00000000..6553152f --- /dev/null +++ b/examples/event-board/react-nano_kit-intl-ssr/src/ui/pages/NotFound.tsx @@ -0,0 +1,51 @@ +import { + useInject, + useSignal +} from '@nano_kit/react' +import { + Link, + title +} from '@nano_kit/react-router' +import { inject } from '@nano_kit/store' +import { Intl$ } from '#src/stores/intl' + +function Messages$() { + const { messages } = inject(Intl$) + + return messages('notFound') +} + +export function Stores$() { + const [$t] = inject(Messages$) + + return [$t] +} + +export function Head$() { + const [$t] = inject(Messages$) + + return [ + title($t.$pageTitle) + ] +} + +export default function NotFoundPage() { + const [$t] = useInject(Messages$) + const t = useSignal($t) + + return ( +
+
+

{t.eyebrow}

+

{t.title}

+

+ {t.description} +

+
+ + + {t.backToEvents} + +
+ ) +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/.env.example b/examples/event-board/svelte-nano_kit-intl-ssr/.env.example new file mode 100644 index 00000000..bfd7e450 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/.env.example @@ -0,0 +1,6 @@ +# Absolute API origin for SSR fetches. +# Defaults: http://localhost:5173 in dev, http://localhost:3001 in production. +VITE_EVENT_BOARD_API_ORIGIN= + +# Production server port (default 3001). +PORT=3001 diff --git a/examples/event-board/svelte-nano_kit-ssr/api/events.js b/examples/event-board/svelte-nano_kit-intl-ssr/api/events.js similarity index 73% rename from examples/event-board/svelte-nano_kit-ssr/api/events.js rename to examples/event-board/svelte-nano_kit-intl-ssr/api/events.js index c14e7bf1..560477d3 100644 --- a/examples/event-board/svelte-nano_kit-ssr/api/events.js +++ b/examples/event-board/svelte-nano_kit-intl-ssr/api/events.js @@ -19,7 +19,8 @@ const events = [ startsAt: new Date('2026-05-12T18:00:00Z').getTime(), location: 'Online', category: 'workshop', - attendees: 24 + guests: 24, + attendeeIds: [] }, { id: '2', @@ -29,7 +30,8 @@ const events = [ startsAt: new Date('2026-05-21T19:30:00Z').getTime(), location: 'Berlin', category: 'meetup', - attendees: 58 + guests: 58, + attendeeIds: [] }, { id: '3', @@ -39,7 +41,8 @@ const events = [ startsAt: new Date('2026-06-03T17:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 102 + guests: 102, + attendeeIds: [] }, { id: '4', @@ -49,7 +52,8 @@ const events = [ startsAt: new Date('2026-06-11T18:30:00Z').getTime(), location: 'Prague', category: 'meetup', - attendees: 41 + guests: 41, + attendeeIds: [] }, { id: '5', @@ -59,7 +63,8 @@ const events = [ startsAt: new Date('2026-06-24T09:00:00Z').getTime(), location: 'Amsterdam', category: 'conference', - attendees: 180 + guests: 180, + attendeeIds: [] }, { id: '6', @@ -69,7 +74,8 @@ const events = [ startsAt: new Date('2026-07-02T16:00:00Z').getTime(), location: 'Online', category: 'webinar', - attendees: 76 + guests: 76, + attendeeIds: [] } ] @@ -116,6 +122,26 @@ function sortEvents(items) { return [...items].sort((a, b) => a.startsAt - b.startsAt || a.id.localeCompare(b.id)) } +/** + * Convert an internal event record into an API payload for a user. + * @param {object} event - Internal event record. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object} Event payload with `attendees` count and personal `going` flag. + */ +function serializeEvent(event, userId) { + const { + guests, + attendeeIds, + ...publicEvent + } = event + + return { + ...publicEvent, + attendees: guests + attendeeIds.length, + going: userId ? attendeeIds.includes(userId) : false + } +} + function validateEventInput(input) { const errors = {} @@ -155,9 +181,10 @@ function validateEventInput(input) { * @param {string | undefined} query.category - Category filter. * @param {string | undefined} query.cursor - Anchor cursor based on `startsAt`. * @param {string | undefined} query.limit - Page size. + * @param {string | undefined} userId - Current user id, if authenticated. * @returns {{ status: number, body: { events: object[], nextCursor?: number } | { error: string } }} API response payload. */ -export function listEvents(query) { +export function listEvents(query, userId) { const q = query.q?.trim().toLowerCase() const category = normalizeCategory(query.category) const cursor = Number(query.cursor || 0) @@ -193,7 +220,7 @@ export function listEvents(query) { return { status: 200, body: { - events: pageItems, + events: pageItems.map(event => serializeEvent(event, userId)), nextCursor: hasMore ? pageItems[pageItems.length - 1]?.startsAt : undefined } } @@ -202,18 +229,22 @@ export function listEvents(query) { /** * Find an event by slug. * @param {string} slug - Event slug. - * @returns {object | null} Event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Event payload or `null` when it does not exist. */ -export function findEvent(slug) { - return events.find(event => event.slug === slug) || null +export function findEvent(slug, userId) { + const event = events.find(item => item.slug === slug) || null + + return event && serializeEvent(event, userId) } /** * Create a new event in the in-memory store. * @param {object} input - Event form payload. + * @param {object | null} user - Current user, if authenticated. * @returns {{ status: number, body: object }} API response payload. */ -export function createEvent(input) { +export function createEvent(input, user) { const errors = validateEventInput(input) if (Object.keys(errors).length > 0) { @@ -233,30 +264,42 @@ export function createEvent(input) { startsAt: input.startsAt, location: input.location.trim(), category: input.category, - attendees: 0 + author: user?.name, + guests: 0, + attendeeIds: [] } events.push(event) return { status: 201, - body: event + body: serializeEvent(event, user?.id) } } /** - * Increment attendees count for an event. + * Register an RSVP for an event. An authenticated user toggles their personal + * attendance, an anonymous request just increments the guest counter. * @param {string} id - Event id. - * @returns {object | null} Updated event object or `null` when it does not exist. + * @param {string | undefined} userId - Current user id, if authenticated. + * @returns {object | null} Updated event payload or `null` when it does not exist. */ -export function rsvpEvent(id) { +export function rsvpEvent(id, userId) { const event = events.find(item => item.id === id) || null if (!event) { return null } - event.attendees += 1 + if (userId) { + if (event.attendeeIds.includes(userId)) { + event.attendeeIds = event.attendeeIds.filter(attendeeId => attendeeId !== userId) + } else { + event.attendeeIds = [...event.attendeeIds, userId] + } + } else { + event.guests += 1 + } - return event + return serializeEvent(event, userId) } diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/api/index.js b/examples/event-board/svelte-nano_kit-intl-ssr/api/index.js new file mode 100644 index 00000000..51ad77c9 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/api/index.js @@ -0,0 +1,157 @@ +import { Hono } from 'hono' +import { + deleteCookie, + getCookie, + setCookie +} from 'hono/cookie' +import { + createEvent, + findEvent, + listEvents, + rsvpEvent +} from './events.js' +import { + authenticateUser, + createSession, + deleteSession, + findUserBySession, + publicUser +} from './users.js' + +const HTTP_BAD_REQUEST = 400 +const HTTP_UNAUTHORIZED = 401 +const HTTP_NOT_FOUND = 404 +const SESSION_COOKIE = 'session' +const SESSION_MAX_AGE = 60 * 60 * 24 * 30 + +/** + * Resolve the current user from the request session cookie. + * @param {import('hono').Context} c + * @returns {object | null} Session user or `null`. + */ +function currentUser(c) { + return findUserBySession(getCookie(c, SESSION_COOKIE)) +} + +export function api() { + const app = new Hono() + + app.post('/api/auth/login', async (c) => { + let body + + try { + body = await c.req.json() + } catch { + return c.json({ + error: 'Expected JSON payload' + }, HTTP_BAD_REQUEST) + } + + const user = authenticateUser(body?.username, body?.password) + + if (!user) { + return c.json({ + error: 'Invalid username or password' + }, HTTP_UNAUTHORIZED) + } + + setCookie(c, SESSION_COOKIE, createSession(user.id), { + path: '/', + httpOnly: true, + sameSite: 'Lax', + maxAge: SESSION_MAX_AGE + }) + + return c.json(publicUser(user)) + }) + + app.post('/api/auth/logout', (c) => { + const token = getCookie(c, SESSION_COOKIE) + + if (token) { + deleteSession(token) + } + + deleteCookie(c, SESSION_COOKIE, { + path: '/' + }) + + return c.json({ + ok: true + }) + }) + + app.get('/api/users/me', (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + + return c.json(publicUser(user)) + }) + + app.get('/api/events', (c) => { + const user = currentUser(c) + const result = listEvents({ + q: c.req.query('q'), + category: c.req.query('category'), + cursor: c.req.query('cursor'), + limit: c.req.query('limit') + }, user?.id) + + return c.json(result.body, result.status) + }) + + app.get('/api/events/:slug', (c) => { + const user = currentUser(c) + const event = findEvent(c.req.param('slug'), user?.id) + + if (!event) { + return c.json(null, HTTP_NOT_FOUND) + } + + return c.json(event) + }) + + app.post('/api/events', async (c) => { + const user = currentUser(c) + + if (!user) { + return c.json({ + error: 'Unauthenticated' + }, HTTP_UNAUTHORIZED) + } + + let body + + try { + body = await c.req.json() + } catch { + return c.json({ + errors: { + form: 'Expected JSON payload' + } + }, HTTP_BAD_REQUEST) + } + + const result = createEvent(body, user) + + return c.json(result.body, result.status) + }) + + app.post('/api/events/:id/rsvp', (c) => { + const user = currentUser(c) + const event = rsvpEvent(c.req.param('id'), user?.id) + + if (!event) { + return c.json(null, HTTP_NOT_FOUND) + } + + return c.json(event) + }) + + return app +} diff --git a/examples/event-board/svelte-nano_kit-ssr/api/server.js b/examples/event-board/svelte-nano_kit-intl-ssr/api/server.js similarity index 100% rename from examples/event-board/svelte-nano_kit-ssr/api/server.js rename to examples/event-board/svelte-nano_kit-intl-ssr/api/server.js diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/api/users.js b/examples/event-board/svelte-nano_kit-intl-ssr/api/users.js new file mode 100644 index 00000000..4b7c6610 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/api/users.js @@ -0,0 +1,87 @@ +/** + * Demo users for the mock API. Credentials are shown on the login form. + */ +const users = [ + { + id: 'u1', + username: 'ada', + password: 'lovelace', + name: 'Ada Lovelace' + }, + { + id: 'u2', + username: 'grace', + password: 'hopper', + name: 'Grace Hopper' + } +] +/** + * In-memory session storage: token -> user id. + * Sessions reset on server restart, same as the rest of the mock data. + */ +const sessions = new Map() + +/** + * Find a user by credentials. + * @param {string} username + * @param {string} password + * @returns {object | null} Matched user or `null` when credentials are invalid. + */ +export function authenticateUser(username, password) { + if (typeof username !== 'string' || typeof password !== 'string') { + return null + } + + return users.find( + user => user.username === username && user.password === password + ) || null +} + +/** + * Create a session for a user. + * @param {string} userId + * @returns {string} Session token. + */ +export function createSession(userId) { + const token = crypto.randomUUID() + + sessions.set(token, userId) + + return token +} + +/** + * Delete a session by token. + * @param {string} token + */ +export function deleteSession(token) { + sessions.delete(token) +} + +/** + * Find a user by session token. + * @param {string | undefined} token + * @returns {object | null} Session user or `null` when the session is invalid. + */ +export function findUserBySession(token) { + const userId = token && sessions.get(token) + + if (!userId) { + return null + } + + return users.find(user => user.id === userId) || null +} + +/** + * Strip private fields from a user. + * @param {object} user + * @returns {{ id: string, username: string, name: string }} Public user payload. + */ +export function publicUser(user) { + return { + id: user.id, + username: user.username, + name: user.name + } +} diff --git a/examples/event-board/svelte-nano_kit-ssr/oxlint.config.ts b/examples/event-board/svelte-nano_kit-intl-ssr/oxlint.config.ts similarity index 100% rename from examples/event-board/svelte-nano_kit-ssr/oxlint.config.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/oxlint.config.ts diff --git a/examples/event-board/svelte-nano_kit-ssr/package.json b/examples/event-board/svelte-nano_kit-intl-ssr/package.json similarity index 89% rename from examples/event-board/svelte-nano_kit-ssr/package.json rename to examples/event-board/svelte-nano_kit-intl-ssr/package.json index e72344c5..ef57d8c1 100644 --- a/examples/event-board/svelte-nano_kit-ssr/package.json +++ b/examples/event-board/svelte-nano_kit-intl-ssr/package.json @@ -1,5 +1,5 @@ { - "name": "event-board-svelte-nano_kit-ssr", + "name": "event-board-svelte-nano_kit-intl-ssr", "private": true, "version": "0.0.0", "packageManager": "pnpm@10.33.0", @@ -17,6 +17,8 @@ }, "dependencies": { "@hono/node-server": "^1.19.9", + "@nano_kit/intl": "*", + "@nano_kit/platform-web": "*", "@nano_kit/query": "*", "@nano_kit/router": "*", "@nano_kit/store": "*", diff --git a/examples/event-board/svelte-nano_kit-ssr/src/app.css b/examples/event-board/svelte-nano_kit-intl-ssr/src/app.css similarity index 70% rename from examples/event-board/svelte-nano_kit-ssr/src/app.css rename to examples/event-board/svelte-nano_kit-intl-ssr/src/app.css index 50d591aa..31d947af 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/app.css +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/app.css @@ -21,6 +21,8 @@ a { } .app { + display: flex; + flex-direction: column; min-height: 100vh; } @@ -58,10 +60,127 @@ a { font-weight: 700; } +.header__user { + display: inline-flex; + align-items: center; + gap: 16px; + font-size: 15px; + font-weight: 700; +} + +.button_link { + border: 0; + padding: 0; + background: none; + color: #0b6b4f; + cursor: pointer; + font: inherit; + font-size: 15px; + font-weight: 700; +} + +.button_link:hover { + text-decoration: underline; +} + .main { + flex: 1; padding: 48px 0; } +.footer { + border-top: 1px solid #d9ded4; + background: #ffffff; +} + +.footer__inner { + display: flex; + align-items: center; + justify-content: flex-end; + min-height: 72px; + gap: 24px; + color: #55625a; + font-size: 15px; + font-weight: 700; +} + +.locale-field { + display: inline-flex; + align-items: center; + gap: 12px; +} + +.locale-switcher { + display: inline-flex; + gap: 8px; +} + +.locale-switcher__button { + min-width: 44px; + min-height: 36px; + border: 1px solid #cbd5cf; + border-radius: 8px; + background: #ffffff; + color: #334139; + cursor: pointer; + font: inherit; + font-size: 14px; + font-weight: 700; +} + +.locale-switcher__button:hover { + border-color: #0b6b4f; + color: #0b6b4f; +} + +.locale-switcher__button[aria-pressed="true"] { + border-color: #0b6b4f; + background: #0b6b4f; + color: #ffffff; +} + +.locale-switcher__button[aria-pressed="true"]:hover { + color: #ffffff; +} + +.loading-overlay { + position: fixed; + inset: 0; + z-index: 100; + display: grid; + place-items: center; + background: rgb(245 247 246 / 0.82); + backdrop-filter: blur(3px); +} + +.loading-overlay__content { + display: inline-flex; + align-items: center; + gap: 12px; + border: 1px solid #d9ded4; + border-radius: 8px; + padding: 18px 20px; + background: #ffffff; + color: #334139; + font-size: 15px; + font-weight: 700; +} + +.loading-overlay__spinner { + width: 24px; + height: 24px; + border: 3px solid #cbd5cf; + border-top-color: #0b6b4f; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(1turn); + } +} + .page { display: grid; gap: 32px; diff --git a/examples/event-board/svelte-nano_kit-ssr/src/index.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/index.ts similarity index 78% rename from examples/event-board/svelte-nano_kit-ssr/src/index.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/src/index.ts index b6cfe25b..a745e255 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/index.ts +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/index.ts @@ -1,6 +1,7 @@ import { layout, loadable, + notFound, page } from '@nano_kit/svelte-router' import { routes } from './stores/router' @@ -18,7 +19,9 @@ export { routes } export const pages = [ layout(Layout, [ page('home', loadable(() => import('./ui/pages/Home.svelte'))), + page('login', loadable(() => import('./ui/pages/Login.svelte'))), page('newEvent', loadable(() => import('./ui/pages/NewEvent.svelte'))), - page('event', loadable(() => import('./ui/pages/Event.svelte'))) + page('event', loadable(() => import('./ui/pages/Event.svelte'))), + notFound(loadable(() => import('./ui/pages/NotFound.svelte'))) ]) ] diff --git a/examples/event-board/svelte-nano_kit-ssr/src/server.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/server.ts similarity index 78% rename from examples/event-board/svelte-nano_kit-ssr/src/server.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/src/server.ts index f602b688..d2b72114 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/server.ts +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/server.ts @@ -22,7 +22,18 @@ app.use(`${renderer.base.replace(/(.)\/$/, '$1')}*`, serveStatic({ })) app.get('*', async (c) => { - const result = await renderer.render(c.req.url) + const result = await renderer.render(c.req.url, { + cookie: c.req.header('Cookie'), + acceptLanguage: c.req.header('Accept-Language') + }) + + if (result.setCookieHeaders) { + for (const cookie of result.setCookieHeaders) { + c.header('Set-Cookie', cookie, { + append: true + }) + } + } if (result.redirect) { return c.redirect(result.redirect, result.statusCode as RedirectStatusCode) diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.client.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.client.ts new file mode 100644 index 00000000..d1cc0039 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.client.ts @@ -0,0 +1,22 @@ +import { Injectable$ } from '@nano_kit/store' +import type { ApiService$ } from './api' + +/** + * Browser API transport. Requests stay relative to the current origin, so + * the browser attaches cookies and `Accept-Language` on its own. + */ +export class ClientApiService$ extends Injectable$ implements ApiService$ { + fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.server.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.server.ts new file mode 100644 index 00000000..2675e09b --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.server.ts @@ -0,0 +1,62 @@ +import { + CookieStore$, + serializeCookies +} from '@nano_kit/platform-web' +import { + Injectable$, + inject +} from '@nano_kit/store' +import { IntlService$ } from '../intl' +import type { ApiService$ } from './api' + +/** + * Absolute API origin used by the server renderer. + * Browser requests stay relative to the current origin. + */ +const API_ORIGIN = import.meta.env.VITE_EVENT_BOARD_API_ORIGIN + || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') + +/** + * SSR API transport. Server-to-server requests carry no browser context, so + * the incoming request cookies and locale are forwarded explicitly. + */ +export class ServerApiService$ extends Injectable$ implements ApiService$ { + cookieStore = inject(CookieStore$) + intlService = inject(IntlService$) + + private async getHeaders() { + const headers: Record = {} + const [ + cookie, + locale + ] = await Promise.all([ + serializeCookies(this.cookieStore, ['session']), + this.intlService.getServerLocale() + ]) + + if (cookie) { + headers['Cookie'] = cookie + } + + if (locale) { + headers['Accept-Language'] = locale + } + + return headers + } + + async fetch(url: string, options?: RequestInit) { + return globalThis.fetch(`${API_ORIGIN}/api/${url}`, { + ...options, + headers: { + ...typeof options?.body === 'string' + ? { + 'Content-Type': 'application/json' + } + : undefined, + ...await this.getHeaders(), + ...options?.headers as Record + } + }) + } +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.ts new file mode 100644 index 00000000..59fffb3a --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.ts @@ -0,0 +1,17 @@ +import { ClientApiService$ } from './api.client' +import { ServerApiService$ } from './api.server' + +export interface ApiService$ { + /** + * Fetch wrapper that adds the API base URL, `Content-Type` and forwarded + * request headers + * @param url + * @param options + * @returns Fetch response + */ + fetch(url: string, options?: RequestInit): Promise +} + +export const ApiService$ = import.meta.env.SSR + ? ServerApiService$ + : ClientApiService$ diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.types.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.types.ts new file mode 100644 index 00000000..759840fb --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/api.types.ts @@ -0,0 +1,5 @@ +export const HttpStatus = { + BadRequest: 400, + Unauthorized: 401, + NotFound: 404 +} as const diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/index.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/index.ts new file mode 100644 index 00000000..f133e643 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/api/index.ts @@ -0,0 +1,2 @@ +export * from './api' +export * from './api.types' diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/events.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/events.ts new file mode 100644 index 00000000..f359daf4 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/events.ts @@ -0,0 +1,107 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' +import type { + BoardEvent, + EventsFilter, + EventsPage, + NewEventForm +} from './events.types' +import { + ApiService$, + HttpStatus +} from './api' + +export * from './events.types' + +export class EventsService$ extends Injectable$ { + api = inject(ApiService$) + + /** + * Fetch a cursor-paginated page of events. + * @param filter - Search, category, cursor, and limit filters. + * @returns Events page with optional next cursor. + */ + async fetchEvents(filter: EventsFilter = {}) { + const params = new URLSearchParams() + + if (filter.q) { + params.set('q', filter.q) + } + + if (filter.category) { + params.set('category', filter.category) + } + + if (filter.cursor) { + params.set('cursor', String(filter.cursor)) + } + + if (filter.limit) { + params.set('limit', String(filter.limit)) + } + + const response = await this.api.fetch(`events?${params}`) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as EventsPage + } + + /** + * Fetch a single event by slug. + * @param slug - Event slug from the route. + * @returns Event details, or `null` when the event does not exist. + */ + async fetchEvent(slug: string) { + const response = await this.api.fetch(`events/${slug}`) + + if (response.status === HttpStatus.NotFound) { + return null + } + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent + } + + /** + * Create a new event in the mock API. + * @param event - New event form payload. + * @returns Created event with generated id, slug, and attendees count. + */ + async createEvent(event: NewEventForm) { + const response = await this.api.fetch('events', { + method: 'POST', + body: JSON.stringify(event) + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent + } + + /** + * Mark the current user as going to an event. + * @param id - Event id. + * @returns Updated event with incremented attendees count. + */ + async rsvpEvent(id: string) { + const response = await this.api.fetch(`events/${id}/rsvp`, { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent + } +} diff --git a/examples/event-board/svelte-nano_kit-ssr/src/services/events.types.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/events.types.ts similarity index 95% rename from examples/event-board/svelte-nano_kit-ssr/src/services/events.types.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/src/services/events.types.ts index ce3f3fa3..1d54d127 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/services/events.types.ts +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/events.types.ts @@ -15,7 +15,9 @@ export interface BoardEvent { startsAt: number location: string category: EventCategory + author?: string attendees: number + going: boolean } export interface EventsPage { diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/index.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/index.ts new file mode 100644 index 00000000..822c6dad --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/index.ts @@ -0,0 +1,2 @@ +export * from './intl' +export * from './translations' diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/intl.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/intl.ts new file mode 100644 index 00000000..399c35c4 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/intl.ts @@ -0,0 +1,28 @@ +import { + CookieStore$, + Locales$, + browserLocale +} from '@nano_kit/platform-web' +import { + Injectable$, + inject +} from '@nano_kit/store' +import { + type SupportedLocale, + supportedLocales +} from './translations' + +export class IntlService$ extends Injectable$ { + locales = inject(Locales$) + cookieStore = inject(CookieStore$) + + getBrowserLocale() { + return browserLocale(this.locales, supportedLocales, 'en') + } + + async getServerLocale() { + const locale = (await this.cookieStore.get('locale'))?.value + + return (locale || this.getBrowserLocale()) as SupportedLocale + } +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/en.json b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/en.json new file mode 100644 index 00000000..abc9963a --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/en.json @@ -0,0 +1,109 @@ +{ + "layout": { + "title": "Event Board", + "events": "Events", + "newEvent": "New event", + "language": "Language", + "loading": "Loading...", + "login": "Log in", + "logout": "Log out" + }, + "login": { + "pageTitle": "Log in | Event Board", + "eyebrow": "Account", + "title": "Log in", + "description": "Sign in to create events and RSVP with your name.", + "usernameLabel": "Username", + "passwordLabel": "Password", + "demoHint": "Demo users: ada / lovelace or grace / hopper.", + "submitting": "Logging in...", + "submit": "Log in", + "errors": { + "LOGIN_INVALID_CREDENTIALS": "Invalid username or password.", + "LOGIN_FAILED": "Login failed. Please try again." + } + }, + "home": { + "pageTitle": "Upcoming events | Event Board", + "pageDescription": "Find meetups, workshops, webinars, and conferences.", + "eyebrow": "Upcoming events", + "title": "Find your next frontend event", + "description": "Meetups, workshops, webinars, and conferences collected in one small demo app.", + "search": "Search", + "searchPlaceholder": "Search events", + "category": "Category", + "allCategories": "All categories", + "categories": { + "conference": "conference", + "meetup": "meetup", + "workshop": "workshop", + "webinar": "webinar" + }, + "noEvents": "No events found. Try another search or category.", + "attendees": { + "one": "{count} going", + "other": "{count} going" + }, + "loadMore": "Load more", + "loading": "Loading..." + }, + "event": { + "pageTitle": "Event | Event Board", + "pageDescription": "Event details", + "eventPageTitle": "{title} | Event Board", + "loading": "Loading event...", + "notFoundEyebrow": "Not found", + "notFoundTitle": "Event not found", + "notFoundDescription": "The event \"{slug}\" does not exist or was removed.", + "backToEvents": "Back to events", + "categories": { + "conference": "conference", + "meetup": "meetup", + "workshop": "workshop", + "webinar": "webinar" + }, + "when": "When", + "where": "Where", + "going": "Going", + "saving": "Saving...", + "rsvp": "I'm going", + "rsvpCancel": "I'm not going", + "hostedBy": "Hosted by" + }, + "newEvent": { + "pageTitle": "New event | Event Board", + "eyebrow": "New event", + "title": "Create an event", + "description": "Add an event to the in-memory API and open its public page.", + "shortcut": "Press Ctrl+M or Cmd+M to fill the form with sample data.", + "titleLabel": "Title", + "titlePlaceholder": "Frontend Meetup", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Short talks, demos, and hallway conversations.", + "startsAtLabel": "Date and time", + "categoryLabel": "Category", + "categories": { + "conference": "conference", + "meetup": "meetup", + "workshop": "workshop", + "webinar": "webinar" + }, + "locationLabel": "Location", + "locationPlaceholder": "Online", + "errors": { + "title": "Title is required", + "description": "Description is required", + "startsAt": "Date and time are required", + "location": "Location is required" + }, + "creating": "Creating...", + "create": "Create event" + }, + "notFound": { + "pageTitle": "Page not found | Event Board", + "eyebrow": "Not found", + "title": "Page not found", + "description": "This page does not exist or was moved.", + "backToEvents": "Back to events" + } +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/index.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/index.ts new file mode 100644 index 00000000..7d28ac65 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/index.ts @@ -0,0 +1,17 @@ +export type SupportedLocale = 'en' | 'ru' + +export type Translations = Awaited> + +export const supportedLocales: SupportedLocale[] = ['en', 'ru'] + +export async function load(locale: SupportedLocale) { + let mod + + if (locale === 'en') { + mod = await import('./en.json') + } else if (locale === 'ru') { + mod = await import('./ru.json') + } + + return mod!.default +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/ru.json b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/ru.json new file mode 100644 index 00000000..b9a2167a --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/intl/translations/ru.json @@ -0,0 +1,111 @@ +{ + "layout": { + "title": "Доска Событий", + "events": "События", + "newEvent": "Новое событие", + "language": "Язык", + "loading": "Загрузка...", + "login": "Войти", + "logout": "Выйти" + }, + "login": { + "pageTitle": "Вход | Доска Событий", + "eyebrow": "Аккаунт", + "title": "Вход", + "description": "Войдите, чтобы создавать события и отмечать участие под своим именем.", + "usernameLabel": "Имя пользователя", + "passwordLabel": "Пароль", + "demoHint": "Демо-пользователи: ada / lovelace или grace / hopper.", + "submitting": "Входим...", + "submit": "Войти", + "errors": { + "LOGIN_INVALID_CREDENTIALS": "Неверное имя пользователя или пароль.", + "LOGIN_FAILED": "Не удалось войти. Попробуйте ещё раз." + } + }, + "home": { + "pageTitle": "События | Доска Событий", + "pageDescription": "Митапы, воркшопы, вебинары и конференции.", + "eyebrow": "Ближайшие события", + "title": "Найдите следующее frontend-событие", + "description": "Митапы, воркшопы, вебинары и конференции, собранные в одном небольшом демо-приложении.", + "search": "Поиск", + "searchPlaceholder": "Искать события", + "category": "Категория", + "allCategories": "Все категории", + "categories": { + "conference": "конференция", + "meetup": "митап", + "workshop": "воркшоп", + "webinar": "вебинар" + }, + "noEvents": "События не найдены. Попробуйте другой поиск или категорию.", + "attendees": { + "one": "{count} участник", + "few": "{count} участника", + "many": "{count} участников", + "other": "{count} участника" + }, + "loadMore": "Загрузить еще", + "loading": "Загрузка..." + }, + "event": { + "pageTitle": "Событие | Доска Событий", + "pageDescription": "Детали события", + "eventPageTitle": "{title} | Доска Событий", + "loading": "Загрузка события...", + "notFoundEyebrow": "Не найдено", + "notFoundTitle": "Событие не найдено", + "notFoundDescription": "Событие \"{slug}\" не существует или было удалено.", + "backToEvents": "Назад к событиям", + "categories": { + "conference": "конференция", + "meetup": "митап", + "workshop": "воркшоп", + "webinar": "вебинар" + }, + "when": "Когда", + "where": "Где", + "going": "Участников", + "saving": "Сохранение...", + "rsvp": "Я пойду", + "rsvpCancel": "Я не пойду", + "hostedBy": "Организатор" + }, + "newEvent": { + "pageTitle": "Новое событие | Доска Событий", + "eyebrow": "Новое событие", + "title": "Создайте событие", + "description": "Добавьте событие во внутренний API и откройте его публичную страницу.", + "shortcut": "Нажмите Ctrl+M или Cmd+M, чтобы заполнить форму тестовыми данными.", + "titleLabel": "Название", + "titlePlaceholder": "Frontend Meetup", + "descriptionLabel": "Описание", + "descriptionPlaceholder": "Короткие доклады, демо и разговоры в кулуарах.", + "startsAtLabel": "Дата и время", + "categoryLabel": "Категория", + "categories": { + "conference": "конференция", + "meetup": "митап", + "workshop": "воркшоп", + "webinar": "вебинар" + }, + "locationLabel": "Место", + "locationPlaceholder": "Онлайн", + "errors": { + "title": "Укажите название", + "description": "Укажите описание", + "startsAt": "Укажите дату и время", + "location": "Укажите место" + }, + "creating": "Создание...", + "create": "Создать событие" + }, + "notFound": { + "pageTitle": "Страница не найдена | Доска Событий", + "eyebrow": "Не найдено", + "title": "Страница не найдена", + "description": "Такой страницы не существует или она была перемещена.", + "backToEvents": "Назад к событиям" + } +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/user.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/user.ts new file mode 100644 index 00000000..2a4c7960 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/user.ts @@ -0,0 +1,73 @@ +import { + Injectable$, + inject +} from '@nano_kit/store' +import type { + LoginParams, + User +} from './user.types' +import { + ApiService$, + HttpStatus +} from './api' +import { UserError } from './user.types' + +export * from './user.types' + +export class UserService$ extends Injectable$ { + api = inject(ApiService$) + + /** + * Fetch the current session user. + * @returns Public user payload. + */ + async getUser() { + const response = await this.api.fetch('users/me') + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.Unauthenticated) + } + + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as User + } + + /** + * Login with demo credentials. The API responds with a `Set-Cookie` header, + * so the session cookie is stored by the browser (or by the SSR cookie store). + * @param params - Username and password. + * @returns Public user payload. + */ + async login(params: LoginParams) { + const response = await this.api.fetch('auth/login', { + method: 'POST', + body: JSON.stringify(params) + }) + + if (!response.ok) { + if (response.status === HttpStatus.Unauthorized) { + throw new Error(UserError.LoginInvalidCredentials) + } + + throw new Error(UserError.LoginFailed) + } + + return await response.json() as User + } + + /** + * Logout the current session. The API deletes the session cookie. + */ + async logout() { + const response = await this.api.fetch('auth/logout', { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + } +} diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/services/user.types.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/user.types.ts new file mode 100644 index 00000000..ae9e3454 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/services/user.types.ts @@ -0,0 +1,18 @@ +export const UserError = { + Unauthenticated: 'UNAUTHENTICATED', + LoginFailed: 'LOGIN_FAILED', + LoginInvalidCredentials: 'LOGIN_INVALID_CREDENTIALS' +} as const + +export type UserError = typeof UserError[keyof typeof UserError] + +export interface User { + id: string + username: string + name: string +} + +export interface LoginParams { + username: string + password: string +} diff --git a/examples/event-board/svelte-nano_kit-ssr/src/stores/events.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/events.ts similarity index 87% rename from examples/event-board/svelte-nano_kit-ssr/src/stores/events.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/src/stores/events.ts index 183fc862..cf6685fb 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/stores/events.ts +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/events.ts @@ -25,13 +25,11 @@ import { type EventCategory, type EventsPage, type NewEventForm, - createEvent, - fetchEvent, - fetchEvents, - rsvpEvent + EventsService$ } from '#src/services/events' import { Params$ } from './router' import { Client$ } from './query' +import { User$ } from './user' import { datetimeLocalValue } from './utils' const SEARCH_DEBOUNCE = 600 @@ -49,6 +47,7 @@ export const EventKey = queryKey<[slug: string | undefined], BoardEvent | null>( export const EventEntity = entity('event') export function EventsList$() { + const eventsService = inject(EventsService$) const { $data, infinite @@ -76,7 +75,7 @@ export function EventsList$() { }) }) - return fetchEvents({ + return eventsService.fetchEvents({ cursor, q, category @@ -102,6 +101,7 @@ export function EventsList$() { } export function EventDetails$() { + const eventsService = inject(EventsService$) const { query } = inject(Client$) const { $slug } = inject(Params$) const [ @@ -113,7 +113,7 @@ export function EventDetails$() { [$slug], async slug => ( slug - ? await fetchEvent(slug) + ? await eventsService.fetchEvent(slug) : null ), [entities(EventEntity)] @@ -127,6 +127,7 @@ export function EventDetails$() { } export function NewEventForm$() { + const eventsService = inject(EventsService$) const { mutation, revalidate @@ -198,7 +199,7 @@ export function NewEventForm$() { })) }) - return createEvent(payload) + return eventsService.createEvent(payload) }, [ disabled(not($valid)), @@ -224,6 +225,8 @@ export function NewEventForm$() { } export function RsvpEvent$() { + const eventsService = inject(EventsService$) + const { $user } = inject(User$) const { $data, mutation @@ -235,21 +238,15 @@ export function RsvpEvent$() { $rsvpLoading ] = mutation<[id: string], BoardEvent>( (id, ctx) => { - const key = EventEntity(id) - const previousEvent = $data(key) - - if (previousEvent) { - $data(key, { - ...previousEvent, - attendees: previousEvent.attendees + 1 - }) + const revert = $data(EventEntity(id), event => event && { + ...event, + going: Boolean($user()) && !event.going, + attendees: event.attendees + (event.going ? -1 : 1) + }) - onError(ctx, () => { - $data(key, previousEvent) - }) - } + onError(ctx, revert) - return rsvpEvent(id) + return eventsService.rsvpEvent(id) } ) diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/intl.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/intl.ts new file mode 100644 index 00000000..5092bb1a --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/intl.ts @@ -0,0 +1,42 @@ +import { inject } from '@nano_kit/store' +import { queryKey } from '@nano_kit/query' +import { intl } from '@nano_kit/intl' +import { + CookieStore$, + cookieStored +} from '@nano_kit/platform-web' +import { + type SupportedLocale, + type Translations, + IntlService$, + load, + supportedLocales +} from '../services/intl' +import { Client$ } from './query' + +export const TranslationsKey = queryKey<[locale: SupportedLocale], Translations>('translations') + +export function Intl$() { + const intlService = inject(IntlService$) + const cookieStore = inject(CookieStore$) + const { query } = inject(Client$) + const $locale = cookieStored(cookieStore, { + name: 'locale', + path: '/', + sameSite: 'lax' + }, intlService.getBrowserLocale()) + const { + messages, + $loading + } = intl( + $locale, + query(TranslationsKey, [$locale], load) + ) + + return { + supportedLocales, + messages, + $locale, + $loading + } +} diff --git a/examples/event-board/svelte-nano_kit-ssr/src/stores/query.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/query.ts similarity index 100% rename from examples/event-board/svelte-nano_kit-ssr/src/stores/query.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/src/stores/query.ts diff --git a/examples/event-board/svelte-nano_kit-ssr/src/stores/router.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/router.ts similarity index 88% rename from examples/event-board/svelte-nano_kit-ssr/src/stores/router.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/src/stores/router.ts index a5cb81cf..fa718013 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/stores/router.ts +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/router.ts @@ -12,10 +12,17 @@ import { export const routes = { home: '/', + login: '/login', newEvent: '/events/new', event: '/events/:slug' } as const +export const publicRoutes = new Set([ + 'home', + 'login', + 'event' +]) + export function Params$() { const $location = inject(Location$) const $searchParams = searchParams($location) diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/user.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/user.ts new file mode 100644 index 00000000..87dad0f2 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/user.ts @@ -0,0 +1,80 @@ +import { + inject, + onMountEffect +} from '@nano_kit/store' +import { + keys, + onSuccess, + queryKey +} from '@nano_kit/query' +import { + LocationNavigation$, + Paths$ +} from '@nano_kit/router' +import { + type LoginParams, + type User, + UserError, + UserService$ +} from '#src/services/user' +import { Client$ } from './query' +import { TranslationsKey } from './intl' +import { publicRoutes } from './router' + +export const UserKey = queryKey<[], User>('user') + +export function User$() { + const userService = inject(UserService$) + const [$location, navigation] = inject(LocationNavigation$) + const paths = inject(Paths$) + const { + query, + mutation, + invalidate + } = inject(Client$) + const [ + $user, + $userError, + $userLoading + ] = query(UserKey, [], () => userService.getUser()) + const invalidateSession = () => { + keys(Key => Key !== TranslationsKey && invalidate(Key)) + } + const [ + login, , + $loginError, + $loginLoading + ] = mutation<[params: LoginParams], User>( + (params, ctx) => { + onSuccess(ctx, () => { + invalidateSession() + navigation.replace(paths.home) + }) + + return userService.login(params) + } + ) + const logout = async () => { + await userService.logout() + invalidateSession() + } + + onMountEffect($user, () => { + const route = $location.$route() + const userError = $userError() + + if (userError === UserError.Unauthenticated && route && !publicRoutes.has(route)) { + navigation.replace(paths.login) + } + }) + + return { + login, + logout, + $user, + $userError, + $userLoading, + $loginError, + $loginLoading + } +} diff --git a/examples/event-board/svelte-nano_kit-ssr/src/stores/utils.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/stores/utils.ts similarity index 100% rename from examples/event-board/svelte-nano_kit-ssr/src/stores/utils.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/src/stores/utils.ts diff --git a/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/Event.svelte b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/Event.svelte similarity index 57% rename from examples/event-board/svelte-nano_kit-ssr/src/ui/pages/Event.svelte rename to examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/Event.svelte index 56dc76d4..eab48f32 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/Event.svelte +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/Event.svelte @@ -4,31 +4,63 @@ meta, title } from '@nano_kit/svelte-router' + import { + datetime, + format, + number, + params, + text, + capitalize + } from '@nano_kit/intl' import { EventDetails$ } from '#src/stores/events' + import { Intl$ } from '#src/stores/intl' + + function Messages$() { + const { messages } = inject(Intl$) + + return messages('event', { + eventPageTitle: params({ + title: text() + }), + notFoundDescription: params({ + slug: text() + }), + attendees: format(number()), + eventDate: format(capitalize(datetime({ + dateStyle: 'full', + timeStyle: 'short' + }))) + }) + } + + export function Stores$() { + const [t] = inject(Messages$) + const { $event: event } = inject(EventDetails$) + + return [t, event] + } export function Head$() { const { $event: event } = inject(EventDetails$) + const [t] = inject(Messages$) return [ title(() => { + const messages = t() const item = event() return item - ? `${item.title} | Event Board` - : 'Event | Event Board' + ? messages.eventPageTitle({ + title: item.title + }) + : messages.pageTitle }), meta({ name: 'description', - content: () => event()?.description ?? 'Event details' + content: () => event()?.description ?? t.$pageDescription() }) ] } - - export function Stores$() { - const { $event: event } = inject(EventDetails$) - - return [event] - } + + + +
+
+
+ {$t.title} + + + +
+ {#if $user} + {$user.name} + + {:else} + {$t.login} + {/if} +
+
+
+ +
+ +
+ +
+ +
+ + {#if $loading} +
+
+ + {$t.loading} +
+
+ {/if} +
diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/Login.svelte b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/Login.svelte new file mode 100644 index 00000000..1fd77780 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/Login.svelte @@ -0,0 +1,104 @@ + + + + +
+ + +
+ + + + + {#if $loginError} +
+ {$t.errors($loginError)} +
+ {/if} + + +
+
diff --git a/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/NewEvent.svelte b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/NewEvent.svelte similarity index 72% rename from examples/event-board/svelte-nano_kit-ssr/src/ui/pages/NewEvent.svelte rename to examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/NewEvent.svelte index 37de5819..9beaccef 100644 --- a/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/NewEvent.svelte +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/NewEvent.svelte @@ -1,9 +1,26 @@ @@ -15,7 +32,6 @@ type EventCategory, eventCategories } from '#src/services/events' - import { NewEventForm$ } from '#src/stores/events' const form = getInject(NewEventForm$) const { @@ -29,6 +45,7 @@ $createError: createError, $createLoading: loading } = form + const [t] = getInject(Messages$) function onSubmit(event: SubmitEvent) { event.preventDefault() @@ -58,47 +75,47 @@
{#if $createError} @@ -147,7 +164,7 @@ {/if}
diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/NotFound.svelte b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/NotFound.svelte new file mode 100644 index 00000000..8e5a5612 --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/NotFound.svelte @@ -0,0 +1,46 @@ + + + + +
+ + + + {$t.backToEvents} + +
diff --git a/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/PageLoader.svelte b/examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/PageLoader.svelte similarity index 100% rename from examples/event-board/svelte-nano_kit-ssr/src/ui/pages/PageLoader.svelte rename to examples/event-board/svelte-nano_kit-intl-ssr/src/ui/pages/PageLoader.svelte diff --git a/examples/event-board/svelte-nano_kit-intl-ssr/src/vite-env.d.ts b/examples/event-board/svelte-nano_kit-intl-ssr/src/vite-env.d.ts new file mode 100644 index 00000000..eaca99da --- /dev/null +++ b/examples/event-board/svelte-nano_kit-intl-ssr/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_EVENT_BOARD_API_ORIGIN?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/examples/event-board/svelte-nano_kit-ssr/svelte.config.js b/examples/event-board/svelte-nano_kit-intl-ssr/svelte.config.js similarity index 100% rename from examples/event-board/svelte-nano_kit-ssr/svelte.config.js rename to examples/event-board/svelte-nano_kit-intl-ssr/svelte.config.js diff --git a/examples/event-board/svelte-nano_kit-ssr/tsconfig.json b/examples/event-board/svelte-nano_kit-intl-ssr/tsconfig.json similarity index 92% rename from examples/event-board/svelte-nano_kit-ssr/tsconfig.json rename to examples/event-board/svelte-nano_kit-intl-ssr/tsconfig.json index 3f48a4b2..6a1781bf 100644 --- a/examples/event-board/svelte-nano_kit-ssr/tsconfig.json +++ b/examples/event-board/svelte-nano_kit-intl-ssr/tsconfig.json @@ -4,7 +4,7 @@ "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2023", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, diff --git a/examples/event-board/svelte-nano_kit-ssr/tsconfig.node.json b/examples/event-board/svelte-nano_kit-intl-ssr/tsconfig.node.json similarity index 100% rename from examples/event-board/svelte-nano_kit-ssr/tsconfig.node.json rename to examples/event-board/svelte-nano_kit-intl-ssr/tsconfig.node.json diff --git a/examples/event-board/svelte-nano_kit-ssr/vite.config.ts b/examples/event-board/svelte-nano_kit-intl-ssr/vite.config.ts similarity index 88% rename from examples/event-board/svelte-nano_kit-ssr/vite.config.ts rename to examples/event-board/svelte-nano_kit-intl-ssr/vite.config.ts index f6f43efa..c58efd4b 100644 --- a/examples/event-board/svelte-nano_kit-ssr/vite.config.ts +++ b/examples/event-board/svelte-nano_kit-intl-ssr/vite.config.ts @@ -31,7 +31,11 @@ export default defineConfig(({ command }) => ({ svelte(), ssr({ index: 'src/index.ts', - server: 'src/server.ts' + server: 'src/server.ts', + inject: { + cookieStore: true, + browserLocale: true + } }) ] })) diff --git a/examples/event-board/svelte-nano_kit-ssr/api/index.js b/examples/event-board/svelte-nano_kit-ssr/api/index.js deleted file mode 100644 index 738b5dcc..00000000 --- a/examples/event-board/svelte-nano_kit-ssr/api/index.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Hono } from 'hono' -import { - createEvent, - findEvent, - listEvents, - rsvpEvent -} from './events.js' - -const HTTP_BAD_REQUEST = 400 -const HTTP_NOT_FOUND = 404 - -export function api() { - const app = new Hono() - - app.get('/api/events', (c) => { - const result = listEvents({ - q: c.req.query('q'), - category: c.req.query('category'), - cursor: c.req.query('cursor'), - limit: c.req.query('limit') - }) - - return c.json(result.body, result.status) - }) - - app.get('/api/events/:slug', (c) => { - const event = findEvent(c.req.param('slug')) - - if (!event) { - return c.json(null, HTTP_NOT_FOUND) - } - - return c.json(event) - }) - - app.post('/api/events', async (c) => { - let body - - try { - body = await c.req.json() - } catch { - return c.json({ - errors: { - form: 'Expected JSON payload' - } - }, HTTP_BAD_REQUEST) - } - - const result = createEvent(body) - - return c.json(result.body, result.status) - }) - - app.post('/api/events/:id/rsvp', (c) => { - const event = rsvpEvent(c.req.param('id')) - - if (!event) { - return c.json(null, HTTP_NOT_FOUND) - } - - return c.json(event) - }) - - return app -} diff --git a/examples/event-board/svelte-nano_kit-ssr/src/services/events.ts b/examples/event-board/svelte-nano_kit-ssr/src/services/events.ts deleted file mode 100644 index 0b5aebfd..00000000 --- a/examples/event-board/svelte-nano_kit-ssr/src/services/events.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { - BoardEvent, - EventsFilter, - EventsPage, - NewEventForm -} from './events.types' - -export * from './events.types' - -/** - * Absolute API origin used by the server renderer. - * Browser requests stay relative to the current origin. - */ -const API_ORIGIN = import.meta.env.SSR - ? import.meta.env.VITE_EVENT_BOARD_API_ORIGIN || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') - : '' -const HTTP_NOT_FOUND = 404 - -/** - * Fetch a cursor-paginated page of events. - * @param filter - Search, category, cursor, and limit filters. - * @returns Events page with optional next cursor. - */ -export async function fetchEvents(filter: EventsFilter = {}) { - const params = new URLSearchParams() - - if (filter.q) { - params.set('q', filter.q) - } - - if (filter.category) { - params.set('category', filter.category) - } - - if (filter.cursor) { - params.set('cursor', String(filter.cursor)) - } - - if (filter.limit) { - params.set('limit', String(filter.limit)) - } - - const response = await fetch(`${API_ORIGIN}/api/events?${params}`) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } - - return await response.json() as EventsPage -} - -/** - * Fetch a single event by slug. - * @param slug - Event slug from the route. - * @returns Event details, or `null` when the event does not exist. - */ -export async function fetchEvent(slug: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${slug}`) - - if (response.status === HTTP_NOT_FOUND) { - return null - } - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } - - return await response.json() as BoardEvent -} - -/** - * Create a new event in the mock API. - * @param event - New event form payload. - * @returns Created event with generated id, slug, and attendees count. - */ -export async function createEvent(event: NewEventForm) { - const response = await fetch(`${API_ORIGIN}/api/events`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(event) - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } - - return await response.json() as BoardEvent -} - -/** - * Mark the current user as going to an event. - * @param id - Event id. - * @returns Updated event with incremented attendees count. - */ -export async function rsvpEvent(id: string) { - const response = await fetch(`${API_ORIGIN}/api/events/${id}/rsvp`, { - method: 'POST' - }) - - if (!response.ok) { - throw new Error(response.statusText || `Request failed with status ${response.status}`) - } - - return await response.json() as BoardEvent -} diff --git a/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/Layout.svelte b/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/Layout.svelte deleted file mode 100644 index 1dfd997b..00000000 --- a/examples/event-board/svelte-nano_kit-ssr/src/ui/pages/Layout.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - - -
-
-
- Event Board - - -
-
- -
- -
-
diff --git a/examples/event-board/svelte-nano_kit-ssr/src/vite-env.d.ts b/examples/event-board/svelte-nano_kit-ssr/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2..00000000 --- a/examples/event-board/svelte-nano_kit-ssr/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f38d3d16..6799ccaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,7 +35,7 @@ catalogs: version: 14.6.1 '@types/node': specifier: ^24.0.14 - version: 24.0.14 + version: 24.13.2 '@types/react': specifier: ^19.2.4 version: 19.2.9 @@ -83,7 +83,7 @@ catalogs: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8 + version: 8.1.2 vitest: specifier: ^4.1.4 version: 4.1.4 @@ -116,7 +116,7 @@ importers: devDependencies: '@commitlint/cli': specifier: ^20.0.0 - version: 20.5.0(@types/node@24.0.14)(conventional-commits-parser@6.4.0)(typescript@7.0.2) + version: 20.5.0(@types/node@24.13.2)(conventional-commits-parser@6.4.0)(typescript@7.0.2) '@commitlint/config-conventional': specifier: ^20.0.0 version: 20.5.0 @@ -125,7 +125,7 @@ importers: version: 20.4.3 '@commitlint/cz-commitlint': specifier: ^20.0.0 - version: 20.5.1(@types/node@24.0.14)(commitizen@4.3.1(@types/node@24.0.14)(typescript@7.0.2))(inquirer@8.2.5)(typescript@7.0.2) + version: 20.5.1(@types/node@24.13.2)(commitizen@4.3.1(@types/node@24.13.2)(typescript@7.0.2))(inquirer@8.2.5)(typescript@7.0.2) '@nano_kit/query': specifier: workspace:^ version: link:packages/query @@ -155,7 +155,7 @@ importers: version: 10.1.1 '@types/node': specifier: 'catalog:' - version: 24.0.14 + version: 24.13.2 '@types/react': specifier: 'catalog:' version: 19.2.9 @@ -167,7 +167,7 @@ importers: version: 5.2.2 commitizen: specifier: ^4.2.4 - version: 4.3.1(@types/node@24.0.14)(typescript@7.0.2) + version: 4.3.1(@types/node@24.13.2)(typescript@7.0.2) del-cli: specifier: ^7.0.0 version: 7.0.0 @@ -210,13 +210,13 @@ importers: devDependencies: vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/common: devDependencies: '@types/node': specifier: 'catalog:' - version: 24.0.14 + version: 24.13.2 playwright: specifier: 'catalog:' version: 1.60.0 @@ -225,7 +225,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.0.14)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) examples/cvapp/react-nano_kit: dependencies: @@ -265,7 +265,7 @@ importers: version: 0.0.39 '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) postcss-custom-media: specifier: ^11.0.6 version: 11.0.6(postcss@8.5.16) @@ -283,13 +283,16 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/event-board/common: dependencies: '@hono/node-server': specifier: ^1.19.9 version: 1.19.9(hono@4.12.1) + '@nano_kit/platform-web': + specifier: workspace:^ + version: link:../../../packages/platform-web '@nano_kit/query': specifier: workspace:^ version: link:../../../packages/query @@ -311,10 +314,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) examples/event-board/preact-nano_kit-intl-ssr: dependencies: @@ -357,13 +360,13 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.10.2 - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) typescript: specifier: ^7.0.0 version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/event-board/react-nano_kit-intl-ssr: dependencies: @@ -412,19 +415,25 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) typescript: specifier: ^7.0.0 version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - examples/event-board/svelte-nano_kit-ssr: + examples/event-board/svelte-nano_kit-intl-ssr: dependencies: '@hono/node-server': specifier: ^1.19.9 version: 1.19.9(hono@4.12.1) + '@nano_kit/intl': + specifier: workspace:^ + version: link:../../../packages/intl + '@nano_kit/platform-web': + specifier: workspace:^ + version: link:../../../packages/platform-web '@nano_kit/query': specifier: workspace:^ version: link:../../../packages/query @@ -449,7 +458,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.7 @@ -458,13 +467,13 @@ importers: version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.4.7 - version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/rick-and-morty/common: dependencies: @@ -495,10 +504,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) examples/rick-and-morty/next-app-nano_kit-ssr: dependencies: @@ -532,7 +541,7 @@ importers: devDependencies: '@types/node': specifier: ^24.0.0 - version: 24.0.14 + version: 24.13.2 '@types/react': specifier: ^19.2.4 version: 19.2.9 @@ -575,7 +584,7 @@ importers: devDependencies: '@types/node': specifier: ^24.0.0 - version: 24.0.14 + version: 24.13.2 '@types/react': specifier: ^19.2.4 version: 19.2.9 @@ -615,7 +624,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -627,7 +636,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/rick-and-morty/react-nano_kit-ssr: dependencies: @@ -673,13 +682,13 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) typescript: specifier: ^7.0.0 version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/rick-and-morty/react-tanstack: dependencies: @@ -688,7 +697,7 @@ importers: version: 5.90.20(react@19.2.3) '@tanstack/react-router': specifier: ^1.58.3 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -701,7 +710,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -713,7 +722,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/rick-and-morty/react-tanstack-start-ssr: dependencies: @@ -728,13 +737,13 @@ importers: version: 1.166.12(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.3))(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.168.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': specifier: ^1.167.50 - version: 1.167.50(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 1.167.50(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) clsx: specifier: ^2.1.1 version: 2.1.1 nitro: specifier: 3.0.260415-beta - version: 3.0.260415-beta(chokidar@5.0.0)(idb-keyval@6.2.2)(jiti@2.6.1)(lru-cache@11.5.1)(rollup@4.60.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 3.0.260415-beta(chokidar@5.0.0)(idb-keyval@6.2.2)(jiti@2.6.1)(lru-cache@11.5.1)(rollup@4.60.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) devDependencies: '@types/react': specifier: ^19.2.4 @@ -744,7 +753,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -756,7 +765,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/rick-and-morty/svelte-kit-nano_kit-ssr: dependencies: @@ -784,25 +793,25 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.0 - version: 7.0.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))) + version: 7.0.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))) '@sveltejs/kit': specifier: ^2.0.0 - version: 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) svelte: specifier: ^5.55.5 version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.4.7 - version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/rick-and-morty/svelte-nano_kit: dependencies: @@ -824,7 +833,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.7 @@ -833,13 +842,13 @@ importers: version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.4.7 - version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/rick-and-morty/svelte-nano_kit-ssr: dependencies: @@ -870,7 +879,7 @@ importers: version: 1.19.9(hono@4.12.1) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.7 @@ -879,13 +888,13 @@ importers: version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.4.7 - version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/session/common: dependencies: @@ -907,10 +916,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) examples/session/react-nano_kit-ssr: dependencies: @@ -953,13 +962,13 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) typescript: specifier: ^7.0.0 version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/session/svelte-kit-nano_kit-ssr: dependencies: @@ -978,25 +987,25 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.0 - version: 7.0.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))) + version: 7.0.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))) '@sveltejs/kit': specifier: ^2.0.0 - version: 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) svelte: specifier: ^5.55.5 version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.4.7 - version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/vite-demo/nanoviews: dependencies: @@ -1009,7 +1018,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/vite-demo/solid: dependencies: @@ -1022,31 +1031,31 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vite-plugin-solid: specifier: ^2.11.12 - version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) examples/vite-demo/svelte: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.48.5)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.7 svelte: specifier: ^5.23.1 - version: 5.48.5 + version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.7.2 - version: 4.7.2(picomatch@4.0.4)(svelte@5.48.5)(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/common: dependencies: @@ -1077,10 +1086,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) examples/weather/nanoviews-nano_kit: dependencies: @@ -1102,7 +1111,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/nanoviews-nano_kit-di: dependencies: @@ -1124,7 +1133,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/react-nano_kit: dependencies: @@ -1149,7 +1158,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -1161,7 +1170,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/react-nano_kit-di: dependencies: @@ -1186,7 +1195,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -1198,7 +1207,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/react-nanostores: dependencies: @@ -1223,7 +1232,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -1235,7 +1244,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/react-reatom: dependencies: @@ -1254,7 +1263,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -1266,7 +1275,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/react-tanstack: dependencies: @@ -1282,7 +1291,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) react: specifier: ^19.2.0 version: 19.2.3 @@ -1294,7 +1303,7 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/solid-nanostores: dependencies: @@ -1319,10 +1328,10 @@ importers: version: 7.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vite-plugin-solid: specifier: ^2.11.12 - version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) examples/weather/svelte-nano_kit: dependencies: @@ -1341,7 +1350,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.7 @@ -1350,13 +1359,13 @@ importers: version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.4.7 - version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/svelte-nano_kit-di: dependencies: @@ -1375,7 +1384,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.7 @@ -1384,13 +1393,13 @@ importers: version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.4.7 - version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) examples/weather/svelte-nanostores: dependencies: @@ -1406,22 +1415,22 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.48.5)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.7 svelte: specifier: ^5.15.0 - version: 5.48.5 + version: 5.55.5(@typescript-eslint/types@8.46.4) svelte-check: specifier: ^4.1.1 - version: 4.3.6(picomatch@4.0.4)(svelte@5.48.5)(typescript@6.0.2) + version: 4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: specifier: ^6.0.0 version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) packages/agera: devDependencies: @@ -1439,13 +1448,13 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vite-7: specifier: npm:vite@^7.3.1 version: vite@7.3.2(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/benchmarks: @@ -1479,7 +1488,7 @@ importers: version: 7.8.2 svelte: specifier: ^5.48.5 - version: 5.48.5 + version: 5.55.5(@typescript-eslint/types@8.46.4) tinybench: specifier: ^4.0.1 version: 4.0.1 @@ -1506,10 +1515,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/kida: @@ -1532,10 +1541,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/nanoviews: @@ -1591,10 +1600,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/next-router: @@ -1629,7 +1638,7 @@ importers: version: 19.2.9 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -1653,10 +1662,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/platform-web: @@ -1669,10 +1678,10 @@ importers: version: 12.0.1(size-limit@12.0.1(jiti@2.6.1)) '@vitest/browser': specifier: ^4.1.7 - version: 4.1.7(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + version: 4.1.7(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) '@vitest/browser-playwright': specifier: ^4.1.7 - version: 4.1.7(playwright@1.60.0)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + version: 4.1.7(playwright@1.60.0)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(@vitest/browser@4.1.7)(vitest@4.1.4) @@ -1687,10 +1696,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/preact: @@ -1700,7 +1709,7 @@ importers: version: link:../store '@preact/preset-vite': specifier: 'catalog:' - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@size-limit/preset-small-lib': specifier: 'catalog:' version: 12.0.1(size-limit@12.0.1(jiti@2.6.1)) @@ -1730,10 +1739,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/preact-router: @@ -1749,7 +1758,7 @@ importers: version: link:../store '@preact/preset-vite': specifier: 'catalog:' - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@size-limit/preset-small-lib': specifier: 'catalog:' version: 12.0.1(size-limit@12.0.1(jiti@2.6.1)) @@ -1779,10 +1788,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/preact-ssr: @@ -1805,7 +1814,7 @@ importers: version: link:../store '@preact/preset-vite': specifier: 'catalog:' - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -1820,10 +1829,10 @@ importers: version: 6.6.7(preact@10.29.1) vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/query: @@ -1848,10 +1857,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/react: @@ -1876,7 +1885,7 @@ importers: version: 19.2.9 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -1897,10 +1906,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/react-router: @@ -1931,7 +1940,7 @@ importers: version: 19.2.9 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -1952,10 +1961,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/react-ssr: @@ -1981,7 +1990,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -1996,10 +2005,10 @@ importers: version: 19.2.3(react@19.2.3) vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/router: @@ -2030,10 +2039,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/ssr: @@ -2059,10 +2068,10 @@ importers: version: 7.0.0 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/store: @@ -2085,10 +2094,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/storybook: @@ -2115,7 +2124,7 @@ importers: dependencies: '@storybook/builder-vite': specifier: ^10.3.5 - version: 10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) devDependencies: '@nanoviews/storybook': specifier: workspace:^ @@ -2125,7 +2134,7 @@ importers: version: 10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) publishDirectory: package packages/svelte: @@ -2138,10 +2147,10 @@ importers: version: 12.0.1(size-limit@12.0.1(jiti@2.6.1)) '@sveltejs/vite-plugin-svelte': specifier: 'catalog:' - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@testing-library/svelte': specifier: 'catalog:' - version: 5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + version: 5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -2159,10 +2168,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/svelte-kit: @@ -2182,13 +2191,13 @@ importers: version: link:../store '@sveltejs/kit': specifier: ^2.0.0 - version: 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@7.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@7.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: 'catalog:' - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@testing-library/svelte': specifier: 'catalog:' - version: 5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + version: 5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -2203,10 +2212,10 @@ importers: version: 7.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/svelte-router: @@ -2225,10 +2234,10 @@ importers: version: 2.5.7(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) '@sveltejs/vite-plugin-svelte': specifier: 'catalog:' - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@testing-library/svelte': specifier: 'catalog:' - version: 5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + version: 5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -2246,10 +2255,10 @@ importers: version: 6.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/svelte-ssr: @@ -2272,7 +2281,7 @@ importers: version: link:../svelte-router '@sveltejs/vite-plugin-svelte': specifier: 'catalog:' - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -2290,10 +2299,10 @@ importers: version: 6.0.2 vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package packages/testing-library: @@ -2306,10 +2315,10 @@ importers: version: link:../nanoviews vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + version: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + version: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) publishDirectory: package website: @@ -2345,10 +2354,6 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@astrojs/check@0.9.9': resolution: {integrity: sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==} hasBin: true @@ -2497,26 +2502,14 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.0': - resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} - engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.0': - resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} - engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.0': - resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} @@ -2525,10 +2518,6 @@ packages: resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} @@ -2541,30 +2530,16 @@ packages: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-module-transforms@7.28.6': resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} - engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.28.6': resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} @@ -2581,10 +2556,6 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} - engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.2': resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} @@ -2594,12 +2565,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-jsx@7.25.9': - resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.28.6': resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} @@ -2628,18 +2593,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.0': - resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} @@ -2733,10 +2690,6 @@ packages: resolution: {integrity: sha512-rDDhFdZrK1iHlqCrmbVrdU+jan7zh0v2Ki145hviKIjHI24sglw2AwSJyctjLR4OhBp+YFqcu4n4O2TbuTPThQ==} engines: {node: '>=v18'} - '@commitlint/config-validator@19.8.1': - resolution: {integrity: sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==} - engines: {node: '>=v18'} - '@commitlint/config-validator@20.5.0': resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} engines: {node: '>=v18'} @@ -2752,10 +2705,6 @@ packages: resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} engines: {node: '>=v18'} - '@commitlint/execute-rule@19.8.1': - resolution: {integrity: sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==} - engines: {node: '>=v18'} - '@commitlint/execute-rule@20.0.0': resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} engines: {node: '>=v18'} @@ -2772,10 +2721,6 @@ packages: resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} engines: {node: '>=v18'} - '@commitlint/load@19.8.1': - resolution: {integrity: sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==} - engines: {node: '>=v18'} - '@commitlint/load@20.5.0': resolution: {integrity: sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==} engines: {node: '>=v18'} @@ -2792,10 +2737,6 @@ packages: resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} engines: {node: '>=v18'} - '@commitlint/resolve-extends@19.8.1': - resolution: {integrity: sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==} - engines: {node: '>=v18'} - '@commitlint/resolve-extends@20.5.0': resolution: {integrity: sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==} engines: {node: '>=v18'} @@ -2812,10 +2753,6 @@ packages: resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} engines: {node: '>=v18'} - '@commitlint/types@19.8.1': - resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} - engines: {node: '>=v18'} - '@commitlint/types@20.5.0': resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} engines: {node: '>=v18'} @@ -2887,18 +2824,9 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} @@ -3408,9 +3336,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -3442,12 +3367,6 @@ packages: nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 solid-js: ^1.6.0 - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: @@ -3540,9 +3459,6 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} - '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} @@ -3880,73 +3796,36 @@ packages: resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-android-arm64@1.1.3': resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-arm64@1.1.3': resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.1.3': resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-freebsd-x64@1.1.3': resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.1.3': resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.1.3': resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3954,13 +3833,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.1.3': resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3968,13 +3840,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.1.3': resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3982,13 +3847,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.3': resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3996,13 +3854,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.3': resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4010,13 +3861,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-x64-musl@1.1.3': resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4024,46 +3868,23 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.1.3': resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.1.3': resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.1.3': resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.3': resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4073,9 +3894,6 @@ packages: '@rolldown/pluginutils@1.0.0-beta.40': resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} - '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -4398,10 +4216,6 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tanstack/history@1.154.14': - resolution: {integrity: sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA==} - engines: {node: '>=12'} - '@tanstack/history@1.161.6': resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} engines: {node: '>=20.19'} @@ -4424,13 +4238,6 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.159.5': - resolution: {integrity: sha512-rVb0MtKzP5c0BkWIoFgWBiRAJHYSU3bhsEHbT0cRdRLmlJiw21Awb6VEjgYq3hJiEhowcKKm6J8AdRD/8oZ5dQ==} - engines: {node: '>=12'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.168.25': resolution: {integrity: sha512-4U/E76dc+fYuLixjV1RLNfqrkQoexSL8MqGNpIHOodtvY3fMPGaALrvDVtBDQYBEU4z5r5fHaV6+kclWAVFP9A==} engines: {node: '>=20.19'} @@ -4487,22 +4294,12 @@ packages: vite: optional: true - '@tanstack/react-store@0.8.0': - resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-store@0.9.3': resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.159.4': - resolution: {integrity: sha512-MFzPH39ijNO83qJN3pe7x4iAlhZyqgao3sJIzv3SJ4Pnk12xMnzuDzIAQT/1WV6JolPQEcw0Wr4L5agF8yxoeg==} - engines: {node: '>=12'} - '@tanstack/router-core@1.168.17': resolution: {integrity: sha512-VDq7HCqRK3sdpxoETwYoTXTaYi+OVQC197g1fdzaiZBUmhntfjn+PQc15OzTqNNhf8Menk6r6ftmuphybMKdig==} engines: {node: '>=20.19'} @@ -4575,9 +4372,6 @@ packages: resolution: {integrity: sha512-HwwPixGNenX1mXJwjJBgWHdxuDj9JHclcbYGeJ130oK2gcyo4RX7AdVsGKV9BCdjv81nODyPYHA9GLzxkp6xfg==} engines: {node: '>=22.12.0'} - '@tanstack/store@0.8.0': - resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} - '@tanstack/store@0.9.3': resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} @@ -4670,9 +4464,6 @@ packages: '@tsconfig/svelte@5.0.7': resolution: {integrity: sha512-NOtJF9LQnV7k6bpzcXwL/rXdlFHvAT9e0imrftiMc6/+FUNBHRZ8UngDrM+jciA6ENzFYNoFs8rfwumuGF+Dhw==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -4697,9 +4488,6 @@ packages: '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} - '@types/conventional-commits-parser@5.0.1': - resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} - '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -4742,9 +4530,6 @@ packages: '@types/node@22.16.4': resolution: {integrity: sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==} - '@types/node@24.0.14': - resolution: {integrity: sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==} - '@types/node@24.13.2': resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} @@ -5059,11 +4844,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -5317,10 +5097,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -5493,9 +5269,6 @@ packages: cookie-es@1.2.3: resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} - cookie-es@2.0.0: - resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} - cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} @@ -5522,15 +5295,6 @@ packages: cosmiconfig: '>=9' typescript: '>=5' - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} @@ -5725,12 +5489,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.6.2: - resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} - - devalue@5.8.0: - resolution: {integrity: sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==} - devalue@5.8.1: resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} @@ -5905,9 +5663,6 @@ packages: engines: {node: '>=4'} hasBin: true - esrap@2.2.2: - resolution: {integrity: sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==} - esrap@2.2.5: resolution: {integrity: sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==} peerDependencies: @@ -6088,6 +5843,7 @@ packages: git-raw-commits@5.0.1: resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} engines: {node: '>=18'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true github-slugger@2.0.0: @@ -6499,10 +6255,6 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - is-wsl@3.1.0: - resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} - engines: {node: '>=16'} - is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -6532,10 +6284,6 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -6704,9 +6452,6 @@ packages: lodash.escape@4.0.1: resolution: {integrity: sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==} - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -6725,9 +6470,6 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} @@ -7032,11 +6774,6 @@ packages: resolution: {integrity: sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A==} engines: {node: ^18.0.0 || >=20.0.0} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.15: resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -7355,10 +7092,6 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} - engines: {node: ^10 || ^12 || >=14} - preact-render-to-string@6.6.7: resolution: {integrity: sha512-3XdbsX3+vn9dQW+jJI/FsI9rlkgl6dbeUpqLsChak6jp3j3auFqBCkno7VChbMFs5Q8ylBj6DrUkKRwtVN3nvw==} peerDependencies: @@ -7581,11 +7314,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rolldown@1.1.3: resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7637,10 +7365,6 @@ packages: satteri@0.9.4: resolution: {integrity: sha512-BKob126Tay84diOZsnVNH/Q/c+3njPJTCad3w5zLKa6j8bVjxskPNHDtxrMwYK4bN/RlqUSdMnPwKY4k65EMOQ==} - sax@1.4.4: - resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} - engines: {node: '>=11.0.0'} - sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -7655,11 +7379,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -7895,22 +7614,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - svelte-check@4.3.6: - resolution: {integrity: sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' - - svelte-check@4.4.7: - resolution: {integrity: sha512-JRafFTRmaPUOqmri4u1WuIKgBLiHi6wIaB57i99pmHq5BAc3ioIpzdUN/RX32ij9GhI6ALMHKvnVxu68sFZlag==} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' - svelte-check@4.7.2: resolution: {integrity: sha512-GoS4XJdGswlq0rIT1vtFLzJY1bvHtY37McY9H9Gkm1Ggw/ICdZYn8J/Z8Yi0BEL0i3R4+jtaWVePjyppMlij/A==} engines: {node: '>= 18.0.0'} @@ -7925,10 +7628,6 @@ packages: svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 typescript: ^4.9.4 || ^5.0.0 || ^6.0.0 - svelte@5.48.5: - resolution: {integrity: sha512-NB3o70OxfmnE5UPyLr8uH3IV02Q43qJVAuWigYmsSOYsS0s/rHxP0TF81blG0onF/xkhNvZw4G8NfzIX+By5ZQ==} - engines: {node: '>=18'} - svelte@5.55.5: resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} engines: {node: '>=18'} @@ -7960,9 +7659,6 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tiny-warning@1.0.3: - resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -7974,18 +7670,10 @@ packages: resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==} engines: {node: ^16.14.0 || >= 17.3.0} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -8073,9 +7761,6 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici-types@7.8.0: - resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - undici@7.25.0: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} @@ -8376,49 +8061,6 @@ packages: yaml: optional: true - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@8.1.2: resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -8462,14 +8104,6 @@ packages: yaml: optional: true - vitefu@1.1.1: - resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - peerDependenciesMeta: - vite: - optional: true - vitefu@1.1.3: resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: @@ -8494,6 +8128,7 @@ packages: '@vitest/ui': 4.1.4 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -8769,11 +8404,6 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.31 - '@astrojs/check@0.9.9(prettier@3.8.1)(typescript@6.0.2)': dependencies: '@astrojs/language-server': 2.16.10(prettier@3.8.1)(typescript@6.0.2) @@ -9049,30 +8679,8 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.0': {} - '@babel/compat-data@7.29.0': {} - '@babel/core@7.28.0': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.29.0 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -9093,14 +8701,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.0': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.0 @@ -9113,14 +8713,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/helper-compilation-targets@7.27.2': - dependencies: - '@babel/compat-data': 7.28.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.1 - lru-cache: 5.1.1 - semver: 6.3.1 - '@babel/helper-compilation-targets@7.28.6': dependencies: '@babel/compat-data': 7.29.0 @@ -9135,13 +8727,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-imports@7.28.6': dependencies: '@babel/traverse': 7.29.0 @@ -9149,15 +8734,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -9167,8 +8743,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-string-parser@7.27.1': {} @@ -9177,11 +8751,6 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.6': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.29.0 - '@babel/helpers@7.29.2': dependencies: '@babel/template': 7.28.6 @@ -9191,11 +8760,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -9226,30 +8790,12 @@ snapshots: '@babel/runtime@7.27.6': {} - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 '@babel/parser': 7.29.0 '@babel/types': 7.29.0 - '@babel/traverse@7.28.0': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.27.2 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -9320,14 +8866,14 @@ snapshots: '@colors/colors@1.6.0': {} - '@commitlint/cli@20.5.0(@types/node@24.0.14)(conventional-commits-parser@6.4.0)(typescript@7.0.2)': + '@commitlint/cli@20.5.0(@types/node@24.13.2)(conventional-commits-parser@6.4.0)(typescript@7.0.2)': dependencies: '@commitlint/format': 20.5.0 '@commitlint/lint': 20.5.0 - '@commitlint/load': 20.5.0(@types/node@24.0.14)(typescript@7.0.2) + '@commitlint/load': 20.5.0(@types/node@24.13.2)(typescript@7.0.2) '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0) '@commitlint/types': 20.5.0 - tinyexec: 1.0.2 + tinyexec: 1.2.4 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' @@ -9346,23 +8892,17 @@ snapshots: fast-glob: 3.3.3 read-yaml-file: 2.1.0 - '@commitlint/config-validator@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - ajv: 8.17.1 - optional: true - '@commitlint/config-validator@20.5.0': dependencies: '@commitlint/types': 20.5.0 ajv: 8.17.1 - '@commitlint/cz-commitlint@20.5.1(@types/node@24.0.14)(commitizen@4.3.1(@types/node@24.0.14)(typescript@7.0.2))(inquirer@8.2.5)(typescript@7.0.2)': + '@commitlint/cz-commitlint@20.5.1(@types/node@24.13.2)(commitizen@4.3.1(@types/node@24.13.2)(typescript@7.0.2))(inquirer@8.2.5)(typescript@7.0.2)': dependencies: '@commitlint/ensure': 20.5.0 - '@commitlint/load': 20.5.0(@types/node@24.0.14)(typescript@7.0.2) + '@commitlint/load': 20.5.0(@types/node@24.13.2)(typescript@7.0.2) '@commitlint/types': 20.5.0 - commitizen: 4.3.1(@types/node@24.0.14)(typescript@7.0.2) + commitizen: 4.3.1(@types/node@24.13.2)(typescript@7.0.2) inquirer: 8.2.5 is-plain-obj: 4.1.0 picocolors: 1.1.1 @@ -9380,9 +8920,6 @@ snapshots: lodash.startcase: 4.4.0 lodash.upperfirst: 4.3.1 - '@commitlint/execute-rule@19.8.1': - optional: true - '@commitlint/execute-rule@20.0.0': {} '@commitlint/format@20.5.0': @@ -9393,7 +8930,7 @@ snapshots: '@commitlint/is-ignored@20.5.0': dependencies: '@commitlint/types': 20.5.0 - semver: 7.7.3 + semver: 7.8.5 '@commitlint/lint@20.5.0': dependencies: @@ -9402,31 +8939,14 @@ snapshots: '@commitlint/rules': 20.5.0 '@commitlint/types': 20.5.0 - '@commitlint/load@19.8.1(@types/node@24.0.14)(typescript@7.0.2)': - dependencies: - '@commitlint/config-validator': 19.8.1 - '@commitlint/execute-rule': 19.8.1 - '@commitlint/resolve-extends': 19.8.1 - '@commitlint/types': 19.8.1 - chalk: 5.4.1 - cosmiconfig: 9.0.0(typescript@7.0.2) - cosmiconfig-typescript-loader: 6.1.0(@types/node@24.0.14)(cosmiconfig@9.0.0(typescript@7.0.2))(typescript@7.0.2) - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - lodash.uniq: 4.5.0 - transitivePeerDependencies: - - '@types/node' - - typescript - optional: true - - '@commitlint/load@20.5.0(@types/node@24.0.14)(typescript@7.0.2)': + '@commitlint/load@20.5.0(@types/node@24.13.2)(typescript@7.0.2)': dependencies: '@commitlint/config-validator': 20.5.0 '@commitlint/execute-rule': 20.0.0 '@commitlint/resolve-extends': 20.5.0 '@commitlint/types': 20.5.0 cosmiconfig: 9.0.1(typescript@7.0.2) - cosmiconfig-typescript-loader: 6.1.0(@types/node@24.0.14)(cosmiconfig@9.0.1(typescript@7.0.2))(typescript@7.0.2) + cosmiconfig-typescript-loader: 6.1.0(@types/node@24.13.2)(cosmiconfig@9.0.1(typescript@7.0.2))(typescript@7.0.2) is-plain-obj: 4.1.0 lodash.mergewith: 4.6.2 picocolors: 1.1.1 @@ -9448,21 +8968,11 @@ snapshots: '@commitlint/types': 20.5.0 git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0) minimist: 1.2.8 - tinyexec: 1.0.2 + tinyexec: 1.2.4 transitivePeerDependencies: - conventional-commits-filter - conventional-commits-parser - '@commitlint/resolve-extends@19.8.1': - dependencies: - '@commitlint/config-validator': 19.8.1 - '@commitlint/types': 19.8.1 - global-directory: 4.0.1 - import-meta-resolve: 4.2.0 - lodash.mergewith: 4.6.2 - resolve-from: 5.0.0 - optional: true - '@commitlint/resolve-extends@20.5.0': dependencies: '@commitlint/config-validator': 20.5.0 @@ -9485,12 +8995,6 @@ snapshots: dependencies: escalade: 3.2.0 - '@commitlint/types@19.8.1': - dependencies: - '@types/conventional-commits-parser': 5.0.1 - chalk: 5.4.1 - optional: true - '@commitlint/types@20.5.0': dependencies: conventional-commits-parser: 6.4.0 @@ -9500,7 +9004,7 @@ snapshots: dependencies: '@simple-libs/child-process-utils': 1.0.1 '@simple-libs/stream-utils': 1.2.0 - semver: 7.7.3 + semver: 7.8.5 optionalDependencies: conventional-commits-parser: 6.4.0 @@ -9557,27 +9061,11 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/core@1.9.2': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 @@ -9750,8 +9238,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.9 - postcss-nested: 6.2.0(postcss@8.5.9) + postcss: 8.5.16 + postcss-nested: 6.2.0(postcss@8.5.16) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -9858,7 +9346,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.9.2 + '@emnapi/runtime': 1.11.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -9884,11 +9372,6 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.29': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -9943,13 +9426,6 @@ snapshots: nanostores: 0.11.4 solid-js: 1.9.11 - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': - dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@tybys/wasm-util': 0.10.1 - optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -10014,8 +9490,6 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@oxc-project/types@0.124.0': {} - '@oxc-project/types@0.137.0': {} '@oxlint-tsgolint/darwin-arm64@0.23.0': @@ -10159,19 +9633,19 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) - '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) debug: 4.4.3 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vite-prerender-plugin: 0.5.13(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite-prerender-plugin: 0.5.13(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact @@ -10186,15 +9660,15 @@ snapshots: '@prefresh/utils@1.2.1': {} - '@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@prefresh/babel-plugin': 0.5.3 '@prefresh/core': 1.5.9(preact@10.29.1) '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 preact: 10.29.1 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -10261,85 +9735,42 @@ snapshots: '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 '@resvg/resvg-js-win32-x64-msvc': 2.6.2 - '@rolldown/binding-android-arm64@1.0.0-rc.15': - optional: true - '@rolldown/binding-android-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - optional: true - '@rolldown/binding-darwin-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - optional: true - '@rolldown/binding-darwin-x64@1.1.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - optional: true - '@rolldown/binding-freebsd-x64@1.1.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - optional: true - '@rolldown/binding-linux-arm64-musl@1.1.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - optional: true - '@rolldown/binding-linux-x64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - optional: true - '@rolldown/binding-linux-x64-musl@1.1.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - optional: true - '@rolldown/binding-openharmony-arm64@1.1.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - optional: true - '@rolldown/binding-wasm32-wasi@1.1.3': dependencies: '@emnapi/core': 1.11.1 @@ -10347,22 +9778,14 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - optional: true - '@rolldown/binding-win32-x64-msvc@1.1.3': optional: true '@rolldown/pluginutils@1.0.0-beta.40': {} - '@rolldown/pluginutils@1.0.0-rc.15': {} - '@rolldown/pluginutils@1.0.0-rc.7': {} '@rolldown/pluginutils@1.0.1': {} @@ -10535,25 +9958,25 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/builder-vite@10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@storybook/builder-vite@10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: - '@storybook/csf-plugin': 10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@storybook/csf-plugin': 10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) storybook: 10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) ts-dedent: 2.2.0 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@storybook/csf-plugin@10.3.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: storybook: 10.3.5(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.1 rollup: 4.60.1 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) '@storybook/global@5.0.0': {} @@ -10571,23 +9994,23 @@ snapshots: estraverse: 5.3.0 picomatch: 4.0.4 - '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.8(acorn@8.17.0)': dependencies: - acorn: 8.15.0 + acorn: 8.17.0 - '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))': + '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))': dependencies: - '@sveltejs/kit': 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@sveltejs/kit': 2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) - '@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.17.0) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@types/cookie': 0.6.0 - acorn: 8.15.0 + acorn: 8.17.0 cookie: 0.6.0 - devalue: 5.8.0 + devalue: 5.8.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -10595,19 +10018,19 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.55.5(@typescript-eslint/types@8.46.4) - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) optionalDependencies: typescript: 6.0.2 - '@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@7.0.2)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@7.0.2)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.17.0) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@types/cookie': 0.6.0 - acorn: 8.15.0 + acorn: 8.17.0 cookie: 0.6.0 - devalue: 5.8.0 + devalue: 5.8.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -10615,7 +10038,7 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.55.5(@typescript-eslint/types@8.46.4) - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) optionalDependencies: typescript: 7.0.2 @@ -10626,35 +10049,24 @@ snapshots: chokidar: 5.0.0 kleur: 4.1.5 sade: 1.8.1 - semver: 7.7.3 + semver: 7.8.5 svelte: 5.55.5(@typescript-eslint/types@8.46.4) svelte2tsx: 0.7.55(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2) typescript: 6.0.2 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.48.5)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': - dependencies: - deepmerge: 4.3.1 - magic-string: 0.30.21 - obug: 2.1.1 - svelte: 5.48.5 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) - - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 svelte: 5.55.5(@typescript-eslint/types@8.46.4) - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 - '@tanstack/history@1.154.14': {} - '@tanstack/history@1.161.6': {} '@tanstack/query-core@5.90.20': {} @@ -10675,17 +10087,6 @@ snapshots: transitivePeerDependencies: - '@tanstack/router-core' - '@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/history': 1.154.14 - '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.159.4 - isbot: 5.1.34 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/history': 1.161.6 @@ -10703,7 +10104,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/react-start-rsc@0.0.29(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@tanstack/react-start-rsc@0.0.29(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@tanstack/react-router': 1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start-server': 1.166.44(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -10711,7 +10112,7 @@ snapshots: '@tanstack/router-utils': 1.161.7 '@tanstack/start-client-core': 1.167.20 '@tanstack/start-fn-stubs': 1.161.6 - '@tanstack/start-plugin-core': 1.169.6(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.5(srvx@0.11.15))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@tanstack/start-plugin-core': 1.169.6(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.5(srvx@0.11.15))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tanstack/start-server-core': 1.167.22(crossws@0.4.5(srvx@0.11.15)) '@tanstack/start-storage-context': 1.166.31 pathe: 2.0.3 @@ -10737,21 +10138,21 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/react-start@1.167.50(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@tanstack/react-start@1.167.50(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@tanstack/react-router': 1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start-client': 1.166.43(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-rsc': 0.0.29(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@tanstack/react-start-rsc': 0.0.29(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tanstack/react-start-server': 1.166.44(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-utils': 1.161.7 '@tanstack/start-client-core': 1.167.20 - '@tanstack/start-plugin-core': 1.169.6(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.5(srvx@0.11.15))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@tanstack/start-plugin-core': 1.169.6(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.5(srvx@0.11.15))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tanstack/start-server-core': 1.167.22(crossws@0.4.5(srvx@0.11.15)) pathe: 2.0.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) transitivePeerDependencies: - '@rspack/core' - crossws @@ -10760,13 +10161,6 @@ snapshots: - vite-plugin-solid - webpack - '@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/store': 0.8.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - use-sync-external-store: 1.6.0(react@19.2.3) - '@tanstack/react-store@0.9.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/store': 0.9.3 @@ -10774,16 +10168,6 @@ snapshots: react-dom: 19.2.3(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@tanstack/router-core@1.159.4': - dependencies: - '@tanstack/history': 1.154.14 - '@tanstack/store': 0.8.0 - cookie-es: 2.0.0 - seroval: 1.5.0 - seroval-plugins: 1.5.0(seroval@1.5.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/router-core@1.168.17': dependencies: '@tanstack/history': 1.161.6 @@ -10804,7 +10188,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.28(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@tanstack/router-plugin@1.167.28(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -10821,8 +10205,8 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vite-plugin-solid: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite-plugin-solid: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -10841,7 +10225,7 @@ snapshots: babel-dead-code-elimination: 1.0.12 diff: 8.0.3 pathe: 2.0.3 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 transitivePeerDependencies: - supports-color @@ -10854,7 +10238,7 @@ snapshots: '@tanstack/start-fn-stubs@1.161.6': {} - '@tanstack/start-plugin-core@1.169.6(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.5(srvx@0.11.15))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@tanstack/start-plugin-core@1.169.6(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.5(srvx@0.11.15))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.29.0 @@ -10862,7 +10246,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.168.17 '@tanstack/router-generator': 1.166.36 - '@tanstack/router-plugin': 1.167.28(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@tanstack/router-plugin': 1.167.28(@tanstack/react-router@1.168.25(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@tanstack/router-utils': 1.161.7 '@tanstack/start-client-core': 1.167.20 '@tanstack/start-server-core': 1.167.22(crossws@0.4.5(srvx@0.11.15)) @@ -10874,13 +10258,13 @@ snapshots: seroval: 1.5.0 source-map: 0.7.6 srvx: 0.11.15 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 ufo: 1.6.3 - vitefu: 1.1.3(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + vitefu: 1.1.3(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) xmlbuilder2: 4.0.3 zod: 3.25.76 optionalDependencies: - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) transitivePeerDependencies: - '@tanstack/react-router' - crossws @@ -10903,15 +10287,13 @@ snapshots: dependencies: '@tanstack/router-core': 1.168.17 - '@tanstack/store@0.8.0': {} - '@tanstack/store@0.9.3': {} '@tanstack/virtual-file-routes@1.161.7': {} '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/runtime': 7.27.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -10959,14 +10341,14 @@ snapshots: dependencies: svelte: 5.55.5(@typescript-eslint/types@8.46.4) - '@testing-library/svelte@5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4)': + '@testing-library/svelte@5.3.1(svelte@5.55.5(@typescript-eslint/types@8.46.4))(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/svelte-core': 1.0.0(svelte@5.55.5(@typescript-eslint/types@8.46.4)) svelte: 5.55.5(@typescript-eslint/types@8.46.4) optionalDependencies: - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vitest: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vitest: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: @@ -10994,11 +10376,6 @@ snapshots: '@tsconfig/svelte@5.0.7': {} - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -11033,11 +10410,6 @@ snapshots: dependencies: '@types/deep-eql': 4.0.2 - '@types/conventional-commits-parser@5.0.1': - dependencies: - '@types/node': 24.0.14 - optional: true - '@types/cookie@0.6.0': {} '@types/debug@4.1.12': @@ -11080,10 +10452,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.0.14': - dependencies: - undici-types: 7.8.0 - '@types/node@24.13.2': dependencies: undici-types: 7.18.2 @@ -11102,7 +10470,7 @@ snapshots: '@types/svg-sprite@0.0.39': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.13.2 '@types/vinyl': 2.0.12 winston: 3.19.0 @@ -11117,13 +10485,13 @@ snapshots: '@types/vinyl@2.0.12': dependencies: '@types/expect': 1.20.4 - '@types/node': 24.0.14 + '@types/node': 24.13.2 '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.13.2 '@typescript-eslint/types@8.46.4': {} @@ -11189,34 +10557,34 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.1(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - '@vitest/browser-playwright@4.1.7(playwright@1.60.0)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4)': + '@vitest/browser-playwright@4.1.7(playwright@1.60.0)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4)': dependencies: - '@vitest/browser': 4.1.7(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) - '@vitest/mocker': 4.1.7(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@vitest/browser': 4.1.7(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + '@vitest/mocker': 4.1.7(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) playwright: 1.60.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + vitest: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.7(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4)': + '@vitest/browser@4.1.7(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.7(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.7(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/utils': 4.1.7 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + vitest: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) ws: 8.20.0 transitivePeerDependencies: - bufferutil @@ -11236,9 +10604,9 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + vitest: 4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.7(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + '@vitest/browser': 4.1.7(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': dependencies: @@ -11252,7 +10620,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@24.0.14)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0) + vitest: 4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/expect@3.2.4': dependencies: @@ -11271,29 +10639,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@24.0.14)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.8(@types/node@24.0.14)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.4(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - '@vitest/mocker@4.1.7(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.7(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -11413,8 +10773,6 @@ snapshots: dependencies: acorn: 8.17.0 - acorn@8.15.0: {} - acorn@8.17.0: {} ajv-draft-04@1.0.0(ajv@8.17.1): @@ -11561,7 +10919,7 @@ snapshots: svgo: 4.0.1 tinyclip: 0.1.15 tinyexec: 1.2.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 ultrahtml: 1.6.0 unifont: 0.7.4 unist-util-visit: 5.1.0 @@ -11629,11 +10987,11 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.0): + babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.28.0) + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/types': 7.29.0 html-entities: 2.3.3 parse5: 7.3.0 @@ -11642,10 +11000,10 @@ snapshots: dependencies: '@babel/core': 7.29.0 - babel-preset-solid@1.9.10(@babel/core@7.28.0)(solid-js@1.9.11): + babel-preset-solid@1.9.10(@babel/core@7.29.0)(solid-js@1.9.11): dependencies: - '@babel/core': 7.28.0 - babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.28.0) + '@babel/core': 7.29.0 + babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.29.0) optionalDependencies: solid-js: 1.9.11 @@ -11748,9 +11106,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: - optional: true - character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -11882,10 +11237,10 @@ snapshots: commander@7.2.0: {} - commitizen@4.3.1(@types/node@24.0.14)(typescript@7.0.2): + commitizen@4.3.1(@types/node@24.13.2)(typescript@7.0.2): dependencies: cachedir: 2.3.0 - cz-conventional-changelog: 3.3.0(@types/node@24.0.14)(typescript@7.0.2) + cz-conventional-changelog: 3.3.0(@types/node@24.13.2)(typescript@7.0.2) dedent: 0.7.0 detect-indent: 6.1.0 find-node-modules: 2.1.3 @@ -11932,8 +11287,6 @@ snapshots: cookie-es@1.2.3: {} - cookie-es@2.0.0: {} - cookie-es@3.1.1: {} cookie@0.6.0: {} @@ -11947,30 +11300,12 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.1.0(@types/node@24.0.14)(cosmiconfig@9.0.0(typescript@7.0.2))(typescript@7.0.2): + cosmiconfig-typescript-loader@6.1.0(@types/node@24.13.2)(cosmiconfig@9.0.1(typescript@7.0.2))(typescript@7.0.2): dependencies: - '@types/node': 24.0.14 - cosmiconfig: 9.0.0(typescript@7.0.2) - jiti: 2.4.2 - typescript: 7.0.2 - optional: true - - cosmiconfig-typescript-loader@6.1.0(@types/node@24.0.14)(cosmiconfig@9.0.1(typescript@7.0.2))(typescript@7.0.2): - dependencies: - '@types/node': 24.0.14 + '@types/node': 24.13.2 cosmiconfig: 9.0.1(typescript@7.0.2) - jiti: 2.4.2 - typescript: 7.0.2 - - cosmiconfig@9.0.0(typescript@7.0.2): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optionalDependencies: + jiti: 2.6.1 typescript: 7.0.2 - optional: true cosmiconfig@9.0.1(typescript@7.0.2): dependencies: @@ -12063,16 +11398,16 @@ snapshots: csstype@3.2.3: {} - cz-conventional-changelog@3.3.0(@types/node@24.0.14)(typescript@7.0.2): + cz-conventional-changelog@3.3.0(@types/node@24.13.2)(typescript@7.0.2): dependencies: chalk: 2.4.2 - commitizen: 4.3.1(@types/node@24.0.14)(typescript@7.0.2) + commitizen: 4.3.1(@types/node@24.13.2)(typescript@7.0.2) conventional-commit-types: 3.0.0 lodash.map: 4.6.0 longest: 2.0.1 word-wrap: 1.2.5 optionalDependencies: - '@commitlint/load': 19.8.1(@types/node@24.0.14)(typescript@7.0.2) + '@commitlint/load': 20.5.0(@types/node@24.13.2)(typescript@7.0.2) transitivePeerDependencies: - '@types/node' - typescript @@ -12169,10 +11504,6 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.6.2: {} - - devalue@5.8.0: {} - devalue@5.8.1: {} devlop@1.1.0: @@ -12386,10 +11717,6 @@ snapshots: esprima@4.0.1: {} - esrap@2.2.2: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - esrap@2.2.5(@typescript-eslint/types@8.46.4): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -12659,7 +11986,7 @@ snapshots: happy-dom@20.8.9: dependencies: - '@types/node': 24.0.14 + '@types/node': 24.13.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -13121,10 +12448,6 @@ snapshots: is-windows@1.0.2: {} - is-wsl@3.1.0: - dependencies: - is-inside-container: 1.0.0 - is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -13150,8 +12473,6 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jiti@2.4.2: {} - jiti@2.6.1: {} jotai@2.17.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.9)(react@19.2.3): @@ -13258,9 +12579,6 @@ snapshots: lodash.escape@4.0.1: {} - lodash.isplainobject@4.0.6: - optional: true - lodash.kebabcase@4.1.1: {} lodash.map@4.6.0: {} @@ -13273,9 +12591,6 @@ snapshots: lodash.startcase@4.4.0: {} - lodash.uniq@4.5.0: - optional: true - lodash.upperfirst@4.3.1: {} lodash@4.17.21: {} @@ -13320,7 +12635,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.8.5 markdown-extensions@2.0.0: {} @@ -13840,8 +13155,6 @@ snapshots: nanoevents@9.1.0: {} - nanoid@3.3.11: {} - nanoid@3.3.15: {} nanoid@5.1.6: {} @@ -13882,7 +13195,7 @@ snapshots: nf3@0.3.16: {} - nitro@3.0.260415-beta(chokidar@5.0.0)(idb-keyval@6.2.2)(jiti@2.6.1)(lru-cache@11.5.1)(rollup@4.60.1)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): + nitro@3.0.260415-beta(chokidar@5.0.0)(idb-keyval@6.2.2)(jiti@2.6.1)(lru-cache@11.5.1)(rollup@4.60.1)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.5(srvx@0.11.15) @@ -13894,14 +13207,14 @@ snapshots: ocache: 0.1.4 ofetch: 2.0.0-alpha.3 ohash: 2.0.11 - rolldown: 1.0.0-rc.15 + rolldown: 1.1.3 srvx: 0.11.15 unenv: 2.0.0-rc.24 unstorage: 2.0.0-alpha.7(chokidar@5.0.0)(db0@0.3.4)(idb-keyval@6.2.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: jiti: 2.6.1 rollup: 4.60.1 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -14112,7 +13425,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -14181,9 +13494,9 @@ snapshots: '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) postcss: 8.5.16 - postcss-nested@6.2.0(postcss@8.5.9): + postcss-nested@6.2.0(postcss@8.5.16): dependencies: - postcss: 8.5.9 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -14193,7 +13506,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -14203,12 +13516,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.9: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - preact-render-to-string@6.6.7(preact@10.29.1): dependencies: preact: 10.29.1 @@ -14507,27 +13814,6 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.15: - dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - rolldown@1.1.3: dependencies: '@oxc-project/types': 0.137.0 @@ -14629,8 +13915,6 @@ snapshots: '@bruits/satteri-win32-arm64-msvc': 0.9.4 '@bruits/satteri-win32-x64-msvc': 0.9.4 - sax@1.4.4: {} - sax@1.6.0: {} scheduler@0.27.0: {} @@ -14639,8 +13923,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.3: {} - semver@7.8.5: {} seroval-plugins@1.5.0(seroval@1.5.0): @@ -14671,7 +13953,7 @@ snapshots: dependencies: '@img/colour': 1.0.0 detect-libc: 2.1.2 - semver: 7.7.3 + semver: 7.8.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -14768,7 +14050,7 @@ snapshots: '@types/node': 24.13.2 '@types/sax': 1.2.7 arg: 5.0.2 - sax: 1.4.4 + sax: 1.6.0 size-limit@12.0.1(jiti@2.6.1): dependencies: @@ -14776,7 +14058,7 @@ snapshots: lilconfig: 3.1.3 nanospinner: 1.2.2 picocolors: 1.1.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 optionalDependencies: jiti: 2.6.1 @@ -14792,8 +14074,8 @@ snapshots: solid-refresh@0.6.3(solid-js@1.9.11): dependencies: - '@babel/generator': 7.28.0 - '@babel/helper-module-imports': 7.27.1 + '@babel/generator': 7.29.1 + '@babel/helper-module-imports': 7.28.6 '@babel/types': 7.29.0 solid-js: 1.9.11 transitivePeerDependencies: @@ -14859,7 +14141,7 @@ snapshots: esbuild: 0.27.3 open: 10.2.0 recast: 0.23.11 - semver: 7.7.3 + semver: 7.8.5 use-sync-external-store: 1.6.0(react@19.2.3) ws: 8.20.0 optionalDependencies: @@ -14927,43 +14209,6 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.3.6(picomatch@4.0.4)(svelte@5.48.5)(typescript@6.0.2): - dependencies: - '@jridgewell/trace-mapping': 0.3.29 - chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.4) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.48.5 - typescript: 6.0.2 - transitivePeerDependencies: - - picomatch - - svelte-check@4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.4) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.55.5(@typescript-eslint/types@8.46.4) - typescript: 6.0.2 - transitivePeerDependencies: - - picomatch - - svelte-check@4.7.2(picomatch@4.0.4)(svelte@5.48.5)(typescript@6.0.2): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@sveltejs/load-config': 0.2.0 - chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.4) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.48.5 - typescript: 6.0.2 - transitivePeerDependencies: - - picomatch - svelte-check@4.7.2(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.46.4))(typescript@6.0.2): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -14984,36 +14229,18 @@ snapshots: svelte: 5.55.5(@typescript-eslint/types@8.46.4) typescript: 6.0.2 - svelte@5.48.5: - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@types/estree': 1.0.8 - acorn: 8.15.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.6.2 - esm-env: 1.2.2 - esrap: 2.2.2 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.4 - svelte@5.55.5(@typescript-eslint/types@8.46.4): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.17.0) '@types/estree': 1.0.8 '@types/trusted-types': 2.0.7 - acorn: 8.15.0 + acorn: 8.17.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.0 + devalue: 5.8.1 esm-env: 1.2.2 esrap: 2.2.5(@typescript-eslint/types@8.46.4) is-reference: 3.0.3 @@ -15071,23 +14298,14 @@ snapshots: tiny-invariant@1.3.3: {} - tiny-warning@1.0.3: {} - tinybench@2.9.0: {} tinybench@4.0.1: {} tinyclip@0.1.15: {} - tinyexec@1.0.2: {} - tinyexec@1.2.4: {} - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -15129,7 +14347,7 @@ snapshots: typescript-auto-import-cache@0.3.6: dependencies: - semver: 7.7.3 + semver: 7.8.5 typescript@6.0.2: {} @@ -15166,8 +14384,6 @@ snapshots: undici-types@7.18.2: {} - undici-types@7.8.0: {} - undici@7.25.0: {} unenv@2.0.0-rc.24: @@ -15251,7 +14467,7 @@ snapshots: unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.15.0 + acorn: 8.17.0 picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 @@ -15328,22 +14544,22 @@ snapshots: remove-trailing-separator: 1.1.0 replace-ext: 1.0.1 - vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): + vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.10(@babel/core@7.28.0)(solid-js@1.9.11) + babel-preset-solid: 1.9.10(@babel/core@7.29.0)(solid-js@1.9.11) merge-anything: 5.1.7 solid-js: 1.9.11 solid-refresh: 0.6.3(solid-js@1.9.11) - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vitefu: 1.1.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) optionalDependencies: '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: - supports-color - vite-prerender-plugin@0.5.13(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): + vite-prerender-plugin@0.5.13(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): dependencies: kolorist: 1.8.0 magic-string: 0.30.21 @@ -15351,16 +14567,16 @@ snapshots: simple-code-frame: 1.3.0 source-map: 0.7.6 stack-trace: 1.0.0-pre2 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) vite@7.3.2(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.9 + postcss: 8.5.16 rollup: 4.60.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 fsevents: 2.3.3 @@ -15368,34 +14584,6 @@ snapshots: lightningcss: 1.32.0 yaml: 2.9.0 - vite@8.0.8(@types/node@24.0.14)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.0.14 - esbuild: 0.28.1 - fsevents: 2.3.3 - jiti: 2.6.1 - yaml: 2.9.0 - - vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.13.2 - esbuild: 0.28.1 - fsevents: 2.3.3 - jiti: 2.6.1 - yaml: 2.9.0 - vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 @@ -15410,22 +14598,14 @@ snapshots: jiti: 2.6.1 yaml: 2.9.0 - vitefu@1.1.1(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): - optionalDependencies: - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - - vitefu@1.1.3(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): - optionalDependencies: - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vitefu@1.1.3(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): optionalDependencies: vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - vitest@4.1.4(@types/node@24.0.14)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0): + vitest@4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.0.14)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.4(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -15439,74 +14619,23 @@ snapshots: picomatch: 4.0.4 std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@24.0.14)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.0.14 - '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) - happy-dom: 20.8.9 - transitivePeerDependencies: - - '@vitejs/devtools' - - esbuild - - jiti - - less - - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - - vitest@4.1.4(@types/node@24.13.2)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0): - dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 - '@vitest/browser-playwright': 4.1.7(playwright@1.60.0)(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) + '@vitest/browser-playwright': 4.1.7(playwright@1.60.0)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.4) '@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.7)(vitest@4.1.4) happy-dom: 20.8.9 transitivePeerDependencies: - - '@vitejs/devtools' - - esbuild - - jiti - - less - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - vitest@4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(esbuild@0.28.1)(happy-dom@20.8.9)(jiti@2.6.1)(yaml@2.9.0): + vitest@4.1.4(@types/node@24.13.2)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.4(vite@8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -15520,28 +14649,17 @@ snapshots: picomatch: 4.0.4 std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) happy-dom: 20.8.9 transitivePeerDependencies: - - '@vitejs/devtools' - - esbuild - - jiti - - less - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml volar-service-css@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -15584,7 +14702,7 @@ snapshots: volar-service-typescript@0.0.70(@volar/language-service@2.4.28): dependencies: path-browserify: 1.0.1 - semver: 7.7.3 + semver: 7.8.5 typescript-auto-import-cache: 0.3.6 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 @@ -15740,7 +14858,7 @@ snapshots: wsl-utils@0.1.0: dependencies: - is-wsl: 3.1.0 + is-wsl: 3.1.1 xmlbuilder2@4.0.3: dependencies: diff --git a/scripts/examples-sync.js b/scripts/examples-sync.js index 2a862d1f..adbb890a 100644 --- a/scripts/examples-sync.js +++ b/scripts/examples-sync.js @@ -1,6 +1,7 @@ /* oxlint-disable no-undef, no-console */ import { cp, + mkdir, readdir, rm, stat @@ -48,16 +49,27 @@ async function exists(path) { } } -async function replaceDir(source, target) { +// Replaces each entry from the source dir, keeping variant-local files that exist only in the target +async function syncDir(source, target) { console.log(` ${source} -> ${target}`) - await rm(target, { - force: true, - recursive: true - }) - await cp(source, target, { + const entries = await readdir(source) + + await mkdir(target, { recursive: true }) + + for (const entry of entries) { + const targetPath = join(target, entry) + + await rm(targetPath, { + force: true, + recursive: true + }) + await cp(join(source, entry), targetPath, { + recursive: true + }) + } } async function syncExample(exampleName) { @@ -70,7 +82,7 @@ async function syncExample(exampleName) { const source = join(sourceExampleDir, dir) if (await exists(source)) { - await replaceDir( + await syncDir( source, join(exampleDir, dir) ) @@ -81,7 +93,7 @@ async function syncExample(exampleName) { const source = join(sourceDir, dir) if (await exists(source)) { - await replaceDir( + await syncDir( source, join(targetSrcDir, dir) ) @@ -89,12 +101,12 @@ async function syncExample(exampleName) { } if (exampleName.includes('nano_kit')) { - await replaceDir( + await syncDir( join(sourceDir, exampleName.includes('ssr') || exampleName.includes('-di') ? 'stores-di' : 'stores'), join(targetSrcDir, 'stores') ) } else if (exampleName.includes('nanostores')) { - await replaceDir( + await syncDir( join(sourceDir, 'nanostores'), join(targetSrcDir, 'stores') ) diff --git a/website/src/content/docs/examples/event-board.mdx b/website/src/content/docs/examples/event-board.mdx index 1b51c8e8..4c03d3c7 100644 --- a/website/src/content/docs/examples/event-board.mdx +++ b/website/src/content/docs/examples/event-board.mdx @@ -9,7 +9,9 @@ import { Tabs, TabItem } from '@astrojs/starlight/components' This example shows a small public event board built with `@nano_kit/store`, `@nano_kit/query`, `@nano_kit/router`, `@nano_kit/intl`, and Nano Kit SSR integrations. -It covers routed pages, URL filters, an in-memory HTTP API, infinite queries, mutations, optimistic updates, entities, page-level `Stores$`, dynamic `Head$`, and a custom Hono production server. +It covers routed pages, URL filters, an in-memory HTTP API, session auth with SSR cookie forwarding, infinite queries, mutations, optimistic updates, entities, page-level `Stores$`, dynamic `Head$`, and a custom Hono production server. + +Demo users: `ada` / `lovelace`, `grace` / `hopper`. The Nano Kit tutorial uses this app as its running project. @@ -40,7 +42,7 @@ The Nano Kit tutorial uses this app as its running project.