diff --git a/app/app.config.ts b/app/app.config.ts index f2d8d00..5299153 100644 --- a/app/app.config.ts +++ b/app/app.config.ts @@ -185,7 +185,7 @@ export default defineAppConfig({ // info: 'i-tabler-info-square-rounded-filled', }, }, - // `seo.siteName`, `header.title` and `github.*` are deliberately NOT defaulted here: modules/config.ts seeds + // `seo.siteName`, `header.title` and `github.*` are deliberately NOT defaulted here: modules/config/ seeds // them into `nuxt.options.appConfig`, and app.config values — even empty strings — would win over those. header: { to: '/', diff --git a/app/app.vue b/app/app.vue index 90c6b72..3c9d7e6 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,6 +1,5 @@ + + diff --git a/app/components/AssistantChat.vue b/app/components/AssistantChat.vue index 4adcc67..591c212 100644 --- a/app/components/AssistantChat.vue +++ b/app/components/AssistantChat.vue @@ -3,7 +3,7 @@ import { DefaultChatTransport, isReasoningUIPart, isTextUIPart, isToolUIPart, ge import { useChat } from '@ai-sdk/vue' import { isPartStreaming, isToolStreaming } from '@nuxt/ui/utils/ai' import rangi from 'comark/plugins/rangi' -import { geistTheme } from '../../utils/geist-theme' +import { geistTheme } from '../../utils/geist' const MAX_INPUT = 1000 diff --git a/app/components/landing/LandingHeroDemo.vue b/app/components/landing/LandingHeroDemo.vue index da3aaef..96cddfa 100644 --- a/app/components/landing/LandingHeroDemo.vue +++ b/app/components/landing/LandingHeroDemo.vue @@ -1,6 +1,6 @@ @@ -32,11 +29,6 @@ provide('navigation', navigation) - - - + diff --git a/app/utils/navigation.ts b/app/utils/navigation.ts index 0cb746b..deaa0ab 100644 --- a/app/utils/navigation.ts +++ b/app/utils/navigation.ts @@ -23,7 +23,7 @@ function walk(items: NavigationItem[], path: string): boolean { return false } -export { findFirstLeaf } from '../../utils/first-leaf' +export { findFirstLeaf } from '../../utils/navigation' export interface BreadcrumbItem { title: string diff --git a/app/utils/search-sections.ts b/app/utils/search-sections.ts deleted file mode 100644 index 4aa65d9..0000000 --- a/app/utils/search-sections.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineContentClientPlugin } from 'comark-content/client' -import { joinURL } from 'ufo' - -/** One search entry per document heading — consumed by `UContentSearch`. */ -export interface SearchSection { - id: string - title: string - titles: string[] - level: number - content: string -} - -interface SearchSectionsClientMethods { - searchSections(): Promise -} - -/** Client half of the `search-sections` serve handler (`server/utils/content.ts`); adds `content.searchSections()`. */ -export const searchSectionsClient = defineContentClientPlugin, SearchSectionsClientMethods>(() => ({ - name: 'search-sections', - setup: ({ options }) => ({ - searchSections: () => options.fetch(joinURL(options.baseURL, options.basePath, 'search-sections')), - }), -})) diff --git a/app/workers/internal/search-logger.ts b/app/workers/internal/search-logger.ts new file mode 100644 index 0000000..64f4ee9 --- /dev/null +++ b/app/workers/internal/search-logger.ts @@ -0,0 +1,69 @@ +/** + * Logging for the search worker. + * + * Triggered by `?debug=search` param. + */ +import type { ContentFile, Logger, RelationalDatabase } from 'comark-content/runtime' + +const PREFIX = '[search:worker]' + +let debug = false + +/** Called on every `warmup`; once on, it stays on for the life of the worker. */ +export function setDebug(value: boolean): void { + debug = debug || value +} + +export function isDebug(): boolean { + return debug +} + +export function log(...args: unknown[]): void { + if (debug) console.info(PREFIX, ...args) +} + +/** Milliseconds since `from`, for log lines. */ +export function since(from: number): string { + return `${(performance.now() - from).toFixed(1)}ms` +} + +/** + * Warn and error are deliberately ungated: the FTS plugin reports a missing snapshot through this + * channel, and that failure is otherwise indistinguishable from "the query matched nothing". + */ +export const logger: Logger = { + debug: (tag, ...args) => log(`${tag}:`, ...args), + info: (tag, ...args) => log(`${tag}:`, ...args), + warn: (tag, ...args) => console.warn(`${PREFIX} ${tag}:`, ...args), + error: (tag, ...args) => console.error(`${PREFIX} ${tag}:`, ...args), +} + +/** + * What a decoded artifact holds: a snapshot decodes to the source's items, the manifest to an object + * keyed by path. `with nodes` is the number that matters — the FTS plugin indexes + * `kind === 'document' && nodes?.length`, so a bodies-less (partial) snapshot builds an empty index. + */ +export function describeArtifact(decoded: unknown): string { + if (Array.isArray(decoded)) { + const items = decoded as ContentFile[] + const documents = items.filter((item) => item.meta.kind === 'document') + const withNodes = documents.filter((item) => item.nodes?.length) + return `${items.length} item(s), ${documents.length} document(s), ${withNodes.length} with nodes` + } + const items = (decoded as { items?: Record } | null)?.items + return `${items ? Object.keys(items).length : 0} manifest item(s)` +} + +/** + * Rows in the FTS plugin's index — the one number that separates "nothing was indexed" from "the + * query found nothing", since `search()` catches SQL errors and returns `[]` either way. Reads the + * plugin's private table, so it is a diagnostic, not something to build on. + */ +export async function indexedRows(database: RelationalDatabase, source: string): Promise { + try { + const rows = await database.all<{ n: number }>('SELECT count(*) as n FROM __fts_search WHERE source = ?', [source]) + return rows?.[0]?.n ?? 'unknown' + } catch (error) { + return `unknown (${error instanceof Error ? error.message : String(error)})` + } +} diff --git a/app/workers/search.ts b/app/workers/search.ts new file mode 100644 index 0000000..e81e295 --- /dev/null +++ b/app/workers/search.ts @@ -0,0 +1,114 @@ +/** + * Search worker: owns the browser-standalone `comark-content` instance (sqlite-wasm FTS5). + * + * Hydrated from the per-commit snapshot artifacts. + */ +import { comarkContent, DEFAULT_CONTENT_NAME, readArtifact } from 'comark-content/runtime' +import sqliteWasm from 'comark-content/database/sqlite-wasm' +import snapshot from 'comark-content/sources/snapshot' +import sqliteFullTextSearch from 'comark-content/plugins/sqlite-full-text-search' +import { ofetch } from 'ofetch' +import { describeArtifact, indexedRows, isDebug, log, logger, setDebug, since } from './internal/search-logger' +import type { CacheArtifact, SearchOptions, SearchResult } from 'comark-content/runtime' + +/** + * Creates a search instance. + */ +function createSearchInstance(fetchArtifact: (path: string) => Promise, apiBase: string) { + const database = sqliteWasm() + return { + database, + content: comarkContent({ + // The first (full-body) tier is what the index is built from; the second (manifest) tier + // is the light one `init()` prefers, so a bare `init()` below doesn't download bodies that + // `search()` is about to fetch anyway via the snapshot tier. + source: snapshot( + () => fetchArtifact(`${apiBase}/snapshot/${DEFAULT_CONTENT_NAME}.json`), + () => fetchArtifact(`${apiBase}/manifest.json`) + ), + plugins: [sqliteFullTextSearch({ database })], + logger, + }), + } +} + +type SearchInstance = ReturnType['content'] + +let instance: SearchInstance | undefined + +/** + * The in-flight hydration. + * + * Ensures only one hydration runs at a time. + */ +let hydration: Promise | undefined + +/** Loads the database. No-op once ready; retries after a failure. */ +export function warmupSearch(apiBase: string, origin: string, debug: boolean): Promise { + setDebug(debug) + if (instance) { + log('warmup ignored — already ready') + return Promise.resolve() + } + hydration ||= loadDatabase(apiBase, origin).catch((error) => { + hydration = undefined // clears the guard so the next warmup can retry + throw error + }) + return hydration +} + +async function loadDatabase(apiBase: string, origin: string): Promise { + const started = performance.now() + try { + const fetchArtifact = async (path: string): Promise => { + const url = new URL(path, origin).href + const fetchStarted = performance.now() + try { + const artifact = await ofetch(url) + if (isDebug()) { + let contents: string + try { + contents = describeArtifact(await readArtifact(artifact)) + } catch (error) { + contents = `undecodable: ${error instanceof Error ? error.message : String(error)}` + } + log(`fetched ${path} in ${since(fetchStarted)} — ${artifact?.size ?? 0} bytes, ${contents}`) + } + return artifact + } catch (error) { + log(`failed ${path} after ${since(fetchStarted)}`, error) + throw error + } + } + + const { database, content } = createSearchInstance(fetchArtifact, apiBase) + + await content.init() + + const indexStarted = performance.now() + await content.search('') // pulls the snapshot in and builds the FTS index + log(`index built in ${since(indexStarted)} — ${await indexedRows(database, DEFAULT_CONTENT_NAME)} row(s)`) + + instance = content + log(`ready in ${since(started)}`) + } catch (error) { + log(`hydration failed after ${since(started)}`, error) + throw error + } +} + +/** Empty until hydration lands. */ +export async function searchContent(query: string, opts?: SearchOptions): Promise { + if (!instance) { + log(`dropped query "${query}" — no instance yet`) + return [] + } + const queryStarted = performance.now() + const results = await instance.search(query, { + limit: 25, + snippet: { columns: ['content'] }, + ...opts, + }) + log(`query "${query}" -> ${results.length} result(s) in ${since(queryStarted)}`) + return results +} diff --git a/docs/cold-page-request.md b/docs/cold-page-request.md index b75ca41..98fa4f7 100644 --- a/docs/cold-page-request.md +++ b/docs/cold-page-request.md @@ -9,7 +9,7 @@ sequenceDiagram participant SSR as Lambda SSR participant ContentRoute as /api/content/** participant Config as Vercel Global Config - participant Refs as Shared ref cache (content:refs) + participant Refs as Shared ref cache (content:refs:v2) participant GH as GitHub participant Content as shared content @@ -24,7 +24,7 @@ sequenceDiagram else no pin or read failed Config-->>Content: undefined Content->>Refs: resolveContentSha(targetBranch, contentDir) - alt cache hit (within 60s TTL) + alt cache hit (within 1h fallback TTL) Refs-->>Content: cached content sha else cache miss Refs->>GH: commits?sha=&path= @@ -51,13 +51,20 @@ its index from GitHub once per content revision, then parses one page. All reads immutable ``. Without a Global Config pin, code-only commits do not rebuild the content instance. -The ref cache is shared across *instances*, so GitHub is hit once per 60s TTL window -rather than once per cold start. It is **not** shared across regions — Vercel's -Runtime Cache is regional (see the note on `refCacheDriver()` in -`server/utils/cache.ts`), so the ceiling is one GitHub call per region per window. -This project runs single-region, which is what makes that distinction academic today. +The production branch pointer is shared across *instances* with a one-hour fallback TTL. The push +webhook refreshes it before purging ISR, so normal cold starts don't need to resolve the branch +through GitHub. If a webhook delivery or refresh fails, a request resolves the branch again after +the fallback TTL instead of serving the old SHA indefinitely. -A ref that doesn't resolve is cached too, for the same window, but **only** when the +Vercel's Runtime Cache is regional (see the note on `refCacheDriver()` in +`server/utils/cache.ts`). This project runs in one region, so the webhook refresh reaches every +instance's shared cache. Additional regions would self-heal when their fallback TTL expires. + +Preview deployments, `/tree/:branch`, `/pr/:number`, and preview authorization decisions use a +600-second TTL because production webhooks don't update them. Negative ref lookups use the same TTL, +including for the production branch, so a temporary GitHub 404 cannot remain cached indefinitely. + +A ref that doesn't resolve is cached too, for 600 seconds, but **only** when the caller asks for it (`resolveContentSha(ref, contentDir, { cacheMisses: true })`) — the public `/tree/:branch` route does, so a nonexistent branch can't be replayed into one GitHub API call per request. The production branch above deliberately does not: @@ -68,8 +75,9 @@ failed request. **On a content push**, `server/api/revalidate.post.ts` forces a fresh `resolveContentSha()` lookup, which writes the latest branch content SHA into the same shared ref cache before fanning out ISR purges for the affected pages. Without a Global Config pin, a freshly-purged page's next render sees -the new SHA instead of waiting out the 60-second TTL. With a pin, the next render stays on the pinned -SHA, while the refreshed branch pointer is ready if the pin is removed. +the new SHA. With a pin, the next render stays on the pinned SHA, while the refreshed branch pointer +is ready if the pin is removed. If the webhook fails, the production pointer refreshes within one +hour. Parsed manifests and bodies live under a parser-version + content-SHA namespace. Vercel Runtime Cache persists across deployments within an environment, so unrelated deployments can reuse diff --git a/modules/config.ts b/modules/config/index.ts similarity index 90% rename from modules/config.ts rename to modules/config/index.ts index 8af5410..de9402d 100644 --- a/modules/config.ts +++ b/modules/config/index.ts @@ -2,10 +2,8 @@ import { existsSync, readdirSync } from 'node:fs' import { addServerPlugin, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' import { defu } from 'defu' import type { ModuleOptions as AgentDiscoveryOptions } from 'nuxt-agent-discovery' -import { resolveContentDir } from '../utils/content-dir' -import { getGitBranch, getGitEnv, getGitRoot, getLocalGitInfo } from '../utils/git' -import { LAYER_ICON_COLLECTIONS } from '../utils/icons' -import { getPackageJsonMetadata, inferSiteURL } from '../utils/meta' +import { getGitBranch, getGitEnv, getGitRoot, getLocalGitInfo } from '../../utils/git' +import { getPackageJsonMetadata, inferSiteURL, resolveContentDir } from './utils' const logger = useLogger('comark-docs') @@ -138,16 +136,6 @@ export default defineNuxtModule({ }, }) - // Drop layer Iconify prefixes from appConfig so @nuxt/icon keeps using the Iconify API (not `/api/_nuxt_icon`). - nuxt.hook('modules:done', () => { - const iconAppConfig = nuxtOptions.appConfig.icon as { customCollections?: string[] } | undefined - if (!iconAppConfig?.customCollections?.length) return - iconAppConfig.customCollections = iconAppConfig.customCollections.filter( - (prefix) => !LAYER_ICON_COLLECTIONS.includes(prefix) - ) - }) - - // Extend Nuxt UI components to make them global and usable in markdown by consumers. nuxt.hook('components:extend', (components) => { const globalComponents = ['UButton', 'UPageHero'] @@ -249,7 +237,7 @@ export default defineNuxtModule({ // Previews are served live (SSR) off Runtime Cache; `/blob/**` is immutable commit HTML. // `/pr/**` follows the PR's head like `/tree/**` follows a branch, so it shares the short TTL. '/tree/**': { isr, robots: 'noindex, nofollow' }, - '/blob/**': { isr: true, robots: 'noindex, nofollow' }, + '/blob/**': { isr: true, robots: 'noindex, nofollow' }, // Immutable since SHA-pinned '/pr/**': { isr, robots: 'noindex, nofollow' }, // Raw markdown mirrors of every page, for agents. '/raw/**': { isr, robots: 'noindex' }, @@ -260,11 +248,17 @@ export default defineNuxtModule({ '/rss.xml': { isr }, // Prerendering would bake the build-time site URL and `docs.version` into it. '/openapi.json': { isr }, - // Fetched on every page hydration (see app.vue) and parses every doc body, so cache it. - '/api/content/blob/*/search-sections': { isr: true }, - '/api/content/tree/*/search-sections': { isr }, - '/api/content/pr/*/search-sections': { isr }, - '/api/content/search-sections': { isr }, + // Scanned from the app at build time, so they only change on deploy. + '/.well-known/skills': { isr: true }, + '/.well-known/skills/**': { isr: true }, + // Per-commit artifacts hydrating the client-side search database (see `useSearch`) + '/api/content/blob/*/manifest.json': { isr: true }, // Immutable since SHA-pinned + '/api/content/blob/*/snapshot/*': { isr: true }, // Immutable since SHA-pinned + '/api/content/tree/*/manifest.json': { isr }, + '/api/content/tree/*/snapshot/*': { isr }, + // `/pr/*` follows the PR head, so it gets the short TTL like `/tree/*`. + '/api/content/pr/*/manifest.json': { isr }, + '/api/content/pr/*/snapshot/*': { isr }, '/api/code-explorer/**': { isr }, '/_payload.json': { headers: { 'cache-control': `public, max-age=${isr}, s-maxage=${isr}, stale-while-revalidate=60` }, @@ -286,6 +280,15 @@ export default defineNuxtModule({ // Consumer-declared rules win per route. nuxt.options.routeRules = defu(nuxt.options.routeRules, rules) as typeof nuxt.options.routeRules + + // Remove once https://github.com/benjamincanac/nuxt-agent-discovery/pull/35 is released. + nuxt.hook('modules:done', () => { + nuxt.hook('prerender:routes', (ctx) => { + for (const route of ctx.routes) { + if (route.startsWith('/.well-known/skills')) ctx.routes.delete(route) + } + }) + }) } }, }) diff --git a/modules/runtime/server/plugins/llms.ts b/modules/config/runtime/server/plugins/llms.ts similarity index 100% rename from modules/runtime/server/plugins/llms.ts rename to modules/config/runtime/server/plugins/llms.ts diff --git a/test/content-dir.test.ts b/modules/config/test/config.test.ts similarity index 60% rename from test/content-dir.test.ts rename to modules/config/test/config.test.ts index 993b7cf..be00c54 100644 --- a/test/content-dir.test.ts +++ b/modules/config/test/config.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest' -import { resolveContentDir } from '../utils/content-dir' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { inferSiteURL, resolveContentDir } from '../utils' describe('resolveContentDir', () => { it('relativises against the git root for an app in a subdirectory', () => { @@ -64,3 +64,54 @@ describe('resolveContentDir', () => { }) }) }) + +describe('inferSiteURL', () => { + const keys = [ + 'NUXT_PUBLIC_SITE_URL', + 'NUXT_SITE_URL', + 'VERCEL_PROJECT_PRODUCTION_URL', + 'VERCEL_BRANCH_URL', + 'VERCEL_URL', + 'URL', + 'CI_PAGES_URL', + 'CF_PAGES_URL', + ] + let saved: Record + + // `Reflect.deleteProperty` rather than `delete process.env[key]`: same effect, + // without tripping `no-dynamic-delete`. + const unset = (key: string) => Reflect.deleteProperty(process.env, key) + + beforeEach(() => { + saved = Object.fromEntries(keys.map((key) => [key, process.env[key]])) + for (const key of keys) unset(key) + }) + + afterEach(() => { + for (const [key, value] of Object.entries(saved)) { + if (value === undefined) unset(key) + else process.env[key] = value + } + }) + + it('returns undefined when nothing is set', () => { + expect(inferSiteURL()).toBeUndefined() + }) + + it('adds https to a bare Vercel host', () => { + process.env.VERCEL_URL = 'my-app-abc123.vercel.app' + expect(inferSiteURL()).toBe('https://my-app-abc123.vercel.app') + }) + + it('prefers the explicit override over the platform value', () => { + process.env.VERCEL_URL = 'my-app-abc123.vercel.app' + process.env.NUXT_PUBLIC_SITE_URL = 'https://docs.example.com' + expect(inferSiteURL()).toBe('https://docs.example.com') + }) + + it('prefers the production URL over the per-branch one', () => { + process.env.VERCEL_BRANCH_URL = 'branch.vercel.app' + process.env.VERCEL_PROJECT_PRODUCTION_URL = 'docs.comark.dev' + expect(inferSiteURL()).toBe('https://docs.comark.dev') + }) +}) diff --git a/utils/content-dir.ts b/modules/config/utils.ts similarity index 59% rename from utils/content-dir.ts rename to modules/config/utils.ts index 1dcc9f1..4648e48 100644 --- a/utils/content-dir.ts +++ b/modules/config/utils.ts @@ -1,4 +1,6 @@ -import { join, normalize, relative } from 'pathe' +import { readFile } from 'node:fs/promises' +import { join, normalize, relative, resolve } from 'pathe' +import { withHttps } from 'ufo' export interface ContentDirInput { rootDir: string @@ -37,3 +39,30 @@ export function resolveContentDir({ rootDir, gitRoot, explicit }: ContentDirInpu return { contentPath, contentDir: 'content', source: 'assumed' } } + +/** Infer the public site URL from the deployment platform env. */ +export function inferSiteURL(): string | undefined { + // https://github.com/unjs/std-env/issues/59 + const url = + process.env.NUXT_PUBLIC_SITE_URL || + process.env.NUXT_SITE_URL || + process.env.VERCEL_PROJECT_PRODUCTION_URL || + process.env.VERCEL_BRANCH_URL || + process.env.VERCEL_URL || + process.env.URL || // Netlify + process.env.CI_PAGES_URL || // GitLab Pages + process.env.CF_PAGES_URL // Cloudflare Pages + + return url ? withHttps(url) : undefined +} + +export async function getPackageJsonMetadata( + dir: string +): Promise<{ name?: string; description?: string; version?: string }> { + try { + const parsed = JSON.parse(await readFile(resolve(dir, 'package.json'), 'utf-8')) + return { name: parsed.name, description: parsed.description, version: parsed.version } + } catch { + return {} + } +} diff --git a/modules/snapshot/index.ts b/modules/snapshot/index.ts new file mode 100644 index 0000000..ce2b70f --- /dev/null +++ b/modules/snapshot/index.ts @@ -0,0 +1,78 @@ +import { mkdir, stat } from 'node:fs/promises' +import { defineNuxtModule, useLogger } from '@nuxt/kit' +import { DEFAULT_CONTENT_NAME } from 'comark-content' +import { writeSnapshots } from 'comark-content/build' +import fs from 'comark-content/sources/fs' +import { join } from 'pathe' +import { createBuildContentInstance } from '../../utils/content' +import { resolveSnapshotSha } from './utils' + +const logger = useLogger('comark-docs') + +/** Where the snapshot lives in the build, and the server-asset namespace it is read back through. */ +const ASSET_BASE = 'comark-content' + +/** + * Writes a build-time snapshot into the function bundle stamped with the commit it was parsed at. + * A cold start at that commit hydrates from it instead of walking the content repository. + * At a later commit it still supplies every unchanged body. + */ +export default defineNuxtModule({ + meta: { name: 'comark-docs:snapshot' }, + setup(_options, nuxt) { + // Do not run in dev or prepare. + if (nuxt.options.dev || nuxt.options._prepare) return + + const dir = join(nuxt.options.buildDir, ASSET_BASE) + + nuxt.hook('modules:done', async () => { + await mkdir(dir, { recursive: true }) + nuxt.options.nitro.serverAssets = [ + ...(nuxt.options.nitro.serverAssets ?? []), + { baseName: ASSET_BASE, dir }, + ] + }) + + nuxt.hook('build:before', async () => { + const { docs } = nuxt.options.runtimeConfig + const { repoRoot, contentDir, contentPath, github } = docs + + const resolveStart = performance.now() + const sha = await resolveSnapshotSha({ + repoRoot, + contentDir, + repo: `${github.owner}/${github.repo}`, + token: docs.githubToken || process.env.NUXT_DOCS_GITHUB_TOKEN || process.env.GITHUB_TOKEN, + warn: (message) => logger.warn(message), + }) + const resolveMs = Math.round(performance.now() - resolveStart) + if (!sha) { + logger.warn( + 'No commit in this checkout could be confirmed to hold the content being built, ' + + 'so no snapshot is shipped: cold starts will walk the content repository.' + ) + return + } + + // `withRef` stamps the artifact with the commit. + // At runtime, even with a different commit, we can reuse unchanged bodies. + const content = createBuildContentInstance({ source: fs(contentPath) }).withRef(sha) + + try { + const writeStart = performance.now() + await writeSnapshots(content, { dir }) + const writeMs = Math.round(performance.now() - writeStart) + + // Size is the number to watch: the snapshot is inlined into the bundle as a string. + // Every cold start that reads it pays for that. + const { size } = await stat(join(dir, DEFAULT_CONTENT_NAME, 'snapshot.json')) + logger.success( + `Content snapshot ${sha.slice(0, 7)}: ${Math.round(size / 1024)} kB parsed and written in ${writeMs}ms ` + + `(ref resolved in ${resolveMs}ms)` + ) + } catch (error) { + logger.warn('Could not write the content snapshot — cold starts will walk the content repository.', error) + } + }) + }, +}) diff --git a/modules/snapshot/test/snapshot.test.ts b/modules/snapshot/test/snapshot.test.ts new file mode 100644 index 0000000..65cd868 --- /dev/null +++ b/modules/snapshot/test/snapshot.test.ts @@ -0,0 +1,116 @@ +import { execFileSync } from 'node:child_process' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { resolveSnapshotSha } from '../utils' + +const SHA = (char: string) => char.repeat(40) + +describe('resolveSnapshotSha', () => { + let repo: string + let contentCommit: string + let head: string + + const run = (...args: string[]) => + execFileSync('git', args, { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() + + const write = async (file: string, body: string) => { + await mkdir(dirname(join(repo, file)), { recursive: true }) + await writeFile(join(repo, file), body, 'utf8') + } + + /** Answers the commits query with `sha` per requested ref; `null` means 404. */ + function stubApi(bySha: Record) { + return vi.fn(async (url: string | URL) => { + const ref = new URL(String(url)).searchParams.get('sha') ?? '' + const answer = bySha[ref] + if (answer === undefined || answer === null) return new Response('[]', { status: 404 }) + return new Response(JSON.stringify([{ sha: answer }]), { status: 200 }) + }) + } + + beforeEach(async () => { + repo = await mkdtemp(join(tmpdir(), 'comark-snapshot-sha-')) + run('init', '-q', '-b', 'main') + run('config', 'user.email', 'test@example.com') + run('config', 'user.name', 'Test') + + await write('content/index.md', '# one\n') + run('add', '-A') + run('commit', '-qm', 'add content') + contentCommit = run('rev-parse', 'HEAD') + + // A later commit that leaves `content/` alone, so HEAD is not the last content commit. + await write('src/app.ts', 'export const a = 1\n') + run('add', '-A') + run('commit', '-qm', 'add code') + head = run('rev-parse', 'HEAD') + }) + + afterEach(async () => { + await rm(repo, { recursive: true, force: true }) + vi.unstubAllGlobals() + }) + + const input = () => ({ repoRoot: repo, contentDir: 'content', repo: 'owner/name', token: 'tok' }) + + it('walks from the built commit, not the branch', async () => { + // The distinction that keeps a mid-build push (or a redeploy of an older commit) from labelling + // the snapshot with content it does not hold. + const fetchMock = stubApi({ [head]: contentCommit, main: SHA('f') }) + vi.stubGlobal('fetch', fetchMock) + + expect(await resolveSnapshotSha(input())).toBe(contentCommit) + + const requested = new URL(String(fetchMock.mock.calls[0]![0])).searchParams + expect(requested.get('sha')).toBe(head) + expect(requested.get('path')).toBe('content') + expect(requested.get('per_page')).toBe('1') + }) + + it('falls back to a tree-verified git answer when the API fails', async () => { + vi.stubGlobal('fetch', stubApi({})) + + // Full history here, so git finds the true commit and its content tree matches HEAD's. + expect(await resolveSnapshotSha(input())).toBe(contentCommit) + }) + + it('skips the API when no repository is known', async () => { + const fetchMock = stubApi({ [head]: SHA('c') }) + vi.stubGlobal('fetch', fetchMock) + + expect(await resolveSnapshotSha({ ...input(), repo: '' })).toBe(contentCommit) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('ships nothing when neither the API nor git can name the content', async () => { + vi.stubGlobal('fetch', stubApi({})) + + expect(await resolveSnapshotSha({ ...input(), contentDir: 'nope' })).toBeUndefined() + }) + + it('warns on the git fallback when the answer is a shallow boundary', async () => { + vi.stubGlobal('fetch', stubApi({})) + const warn = vi.fn() + + // A one-commit repo: its only commit is parentless, which is what a depth-1 clone looks like. + const shallow = await mkdtemp(join(tmpdir(), 'comark-shallow-')) + try { + const at = (...args: string[]) => execFileSync('git', args, { cwd: shallow, stdio: 'ignore' }) + at('init', '-q', '-b', 'main') + at('config', 'user.email', 'test@example.com') + at('config', 'user.name', 'Test') + await mkdir(join(shallow, 'content'), { recursive: true }) + await writeFile(join(shallow, 'content/index.md'), '# one\n', 'utf8') + at('add', '-A') + at('commit', '-qm', 'init') + + expect(await resolveSnapshotSha({ ...input(), repoRoot: shallow, warn })).toMatch(/^[0-9a-f]{40}$/) + expect(warn).toHaveBeenCalledOnce() + expect(warn.mock.calls[0]![0]).toContain('shallow clone boundary') + } finally { + await rm(shallow, { recursive: true, force: true }) + } + }) +}) diff --git a/modules/snapshot/utils.ts b/modules/snapshot/utils.ts new file mode 100644 index 0000000..af03c95 --- /dev/null +++ b/modules/snapshot/utils.ts @@ -0,0 +1,62 @@ +import { getLastCommit, getTreeSha, hasParent, headCommit } from '../../utils/git' +import { fetchLastContentCommit } from '../../utils/github' + +export interface SnapshotShaInput { + /** Repository root of the checkout being built. */ + repoRoot: string + /** Content directory, relative to the repository root. */ + contentDir: string + /** `owner/name` of the content repository. Empty when the checkout has no usable remote. */ + repo: string + /** GitHub token, if the build has one. Without it only the git fallback runs. */ + token?: string + /** Reported to the caller; defaults to `console.warn`. */ + warn?: (message: string) => void +} + +/** {@link fetchLastContentCommit}, but never throwing: a build-time optimization must not fail a build. */ +async function lastContentCommit( + repo: string, + contentDir: string, + ref: string, + token?: string +): Promise { + try { + const sha = await fetchLastContentCommit({ repo, path: contentDir, ref, token }) + // Validated here rather than in the shared query: this one names a directory in the build. + return sha && /^[0-9a-f]{40}$/.test(sha) ? sha : undefined + } catch { + return undefined + } +} + +/** + * The commit whose `contentDir` holds the content being parsed. + * The only ref the snapshot may be stored under. + * The same one `resolveContentSha()` resolves at runtime. + */ +export async function resolveSnapshotSha(input: SnapshotShaInput): Promise { + const { repoRoot, contentDir, repo, token } = input + const warn = input.warn ?? ((message: string) => console.warn(message)) + + const head = headCommit(repoRoot) + const fromApi = head && repo ? await lastContentCommit(repo, contentDir, head, token) : undefined + if (fromApi) return fromApi + + // No API answer: fall back to git, which needs the tree check to be trustworthy. + const parsed = getTreeSha(repoRoot, 'HEAD', contentDir) + const fromGit = getLastCommit(repoRoot, contentDir) + if (!parsed || !fromGit) return undefined + + if (getTreeSha(repoRoot, fromGit, contentDir) !== parsed) return undefined + + if (!hasParent(repoRoot, fromGit)) { + warn( + `Could not reach the GitHub API, and git labels the snapshot ${fromGit.slice(0, 7)}, ` + + `which has no parent in this checkout — a shallow clone boundary.\n` + + ` The snapshot is safe, but probably will not be looked up under that commit at runtime.` + ) + } + + return fromGit +} diff --git a/nuxt.config.ts b/nuxt.config.ts index de6f8c8..3a9397d 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,7 +1,7 @@ import { resolveModulePath } from 'exsolve' import { defineNuxtConfig } from 'nuxt/config' import { createResolver } from 'nuxt/kit' -import { layerIconCollections } from './utils/icons' +import { LAYER_ICON_COLLECTIONS, layerIconAliases } from './utils/icons' const { resolve } = createResolver(import.meta.url) @@ -12,7 +12,7 @@ export default defineNuxtConfig({ // The layer's own modules go first: what config.ts seeds (`site`, `mcp`, `agentDiscovery`) has to be in // place before the modules that read it at setup. Nuxt queues a layer's `modules` before its scanned // `modules/` dir and dedupes by file path, so the extension is what keeps these from installing twice. - resolve('./modules/config.ts'), + resolve('./modules/config/index.ts'), resolve('./modules/css.ts'), '@nuxt/ui', '@comark/nuxt', @@ -23,6 +23,7 @@ export default defineNuxtConfig({ 'nuxt-schema-org', '@nuxtjs/mcp-toolkit', 'nuxt-llms', + 'nuxt-workers', 'nuxt-agent-discovery', ], ignore: ['content/**'], @@ -52,29 +53,31 @@ export default defineNuxtConfig({ }, ogImage: { zeroRuntime: false }, icon: { - provider: 'iconify', - customCollections: layerIconCollections() as never, - clientBundle: { - scan: true, - includeCustomCollections: false - }, + provider: 'server', + fallbackToApi: 'client-only', + serverBundle: { collections: LAYER_ICON_COLLECTIONS }, + clientBundle: { scan: true }, }, vite: { resolve: { alias: { 'beautiful-mermaid': resolveModulePath('beautiful-mermaid', { from: import.meta.url }) }, }, + worker: { format: 'es' }, optimizeDeps: { include: [ 'beautiful-mermaid', 'comark-docs > ai > @ai-sdk/gateway > @vercel/oidc', 'js-yaml' ], + // Pre-bundling would break the wasm/worker assets sqlite loads relative to its module URL. + exclude: ['@sqlite.org/sqlite-wasm'], }, }, llms: { prerender: false, }, nitro: { + alias: layerIconAliases(), // MCP tool handlers reach the request through `useEvent()`. experimental: { asyncContext: true }, vercel: { diff --git a/package.json b/package.json index 486433a..ffb09ea 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,14 @@ "dev:prepare": "nuxt prepare playground" }, "dependencies": { - "@ai-sdk/gateway": "^4.0.62", - "@ai-sdk/vue": "^4.0.77", + "@ai-sdk/gateway": "^4.0.78", + "@ai-sdk/vue": "^4.0.97", "@comark/nuxt": "^0.6.2", - "@iconify-json/lucide": "^1.2.125", - "@iconify-json/simple-icons": "^1.2.93", - "@iconify-json/vscode-icons": "^1.2.74", + "@iconify-json/logos": "^1.2.14", + "@iconify-json/lucide": "^1.2.131", + "@iconify-json/simple-icons": "^1.2.95", + "@iconify-json/unjs": "^1.2.4", + "@iconify-json/vscode-icons": "^1.2.77", "@iconify/vue": "^5.0.1", "@nuxt/kit": "^4.5.2", "@nuxt/ui": "^4.11.1", @@ -53,32 +55,34 @@ "@octokit/webhooks-methods": "^6.0.0", "@opentelemetry/api": "^1.9.1", "@resvg/resvg-js": "^2.6.2", + "@sqlite.org/sqlite-wasm": "3.53.0-build1", "@vercel/analytics": "^2.0.1", - "@vercel/functions": "^3.9.5", + "@vercel/functions": "^3.9.7", "@vercel/global-config": "^1.5.1", "@vercel/otel": "^2.1.3", "@vercel/speed-insights": "^2.0.0", "@vueuse/core": "^14.4.0", - "ai": "^7.0.77", + "ai": "^7.0.97", "beautiful-mermaid": "^1.1.3", "comark": "^0.6.2", - "comark-content": "https://pkg.pr.new/comark-content@baefd4d", + "comark-content": "^0.4.0", "defu": "^6.1.7", "exsolve": "^1.1.1", - "js-yaml": "^5.3.0", - "motion-v": "^2.4.0", + "js-yaml": "^5.4.1", + "motion-v": "^2.4.2", "nuxt-agent-discovery": "^0.5.1", "nuxt-llms": "https://pkg.pr.new/nuxt-content/nuxt-llms/nuxt-llms@f6a9730", "nuxt-og-image": "^6.7.8", "nuxt-schema-org": "^6.3.1", - "nuxt-seo-utils": "^8.4.2", + "nuxt-seo-utils": "^8.5.0", + "nuxt-workers": "^0.1.0", "pathe": "^2.0.3", "rangi": "^2.2.0", "satori": "^0.29.1", "tailwindcss": "^4.3.3", "ufo": "^1.6.4", "unstorage": "^1.17.5", - "zod": "^4.4.3" + "zod": "^4.6.1" }, "peerDependencies": { "nuxt": "^4.5.0" @@ -87,10 +91,10 @@ "@nuxt/devtools-kit": "4.0.0-alpha.9", "@nuxt/eslint-config": "^1.17.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.221.0", - "@opentelemetry/resources": "^2.10.0", - "@opentelemetry/sdk-trace-base": "^2.10.0", - "@opentelemetry/sdk-trace-node": "^2.10.0", - "eslint": "^10.9.0", + "@opentelemetry/resources": "^2.11.0", + "@opentelemetry/sdk-trace-base": "^2.11.0", + "@opentelemetry/sdk-trace-node": "^2.11.0", + "eslint": "^10.10.0", "nuxt": "^4.5.2", "typescript": "^6.0.3", "vitest": "^4.1.11", diff --git a/playground/content/3.concepts/1.architecture.md b/playground/content/3.concepts/1.architecture.md index adf5c82..8cad4f3 100644 --- a/playground/content/3.concepts/1.architecture.md +++ b/playground/content/3.concepts/1.architecture.md @@ -23,7 +23,7 @@ In development, none of this applies: content is read straight from your working Production doesn't read "whatever is on `main` right now." On each server render, comark-docs first checks a connected Vercel Global Config store for a `contentSha` value. When the value exists, every production content read is pinned to that commit. You can use this override to hold production on a reviewed version or roll content back without changing the production branch. See [Pin production content](/deployment/vercel#pin-production-content) for setup and cache timing. -Without a `contentSha` value, the server resolves the latest commit **touching the content directory** on the production branch. A shared, 60-second-TTL cache keeps this to about one GitHub call per minute. If Global Config is unavailable, comark-docs also falls back to this branch-based resolution. +Without a `contentSha` value, the server resolves the latest commit **touching the content directory** on the production branch. The push webhook refreshes the shared pointer before purging affected pages, and a one-hour fallback TTL bounds staleness if the webhook fails. If Global Config is unavailable, comark-docs also falls back to this branch-based resolution. The Global Config pin applies only to the production Vercel environment. Preview deployments continue to follow their target branch, and local development reads from your working tree. @@ -69,7 +69,7 @@ sequenceDiagram The handler verifies the webhook signature with `WEBHOOK_SECRET`, resolves the new content SHA, diffs the file manifests to find affected pages, and purges exactly those from the ISR cache. The next request renders from the new commit — typically live within seconds of the push. -Without the webhook, the site still updates: ISR entries expire on their own after the `isr` window. The webhook just makes it immediate. +The webhook advances the production pointer immediately. If delivery or refresh fails, the pointer expires within one hour and the next server render resolves the latest content commit through GitHub. ## Markdown for agents diff --git a/playground/content/3.concepts/2.versioned-previews.md b/playground/content/3.concepts/2.versioned-previews.md index d054546..91ba73e 100644 --- a/playground/content/3.concepts/2.versioned-previews.md +++ b/playground/content/3.concepts/2.versioned-previews.md @@ -29,7 +29,7 @@ So a commit only renders under `/blob/:sha` when at least one of these holds: - A pull request from your own repository contains it. Contributors with push access could publish a `/tree/` preview anyway, so their PRs need no extra step. - A pull request from a fork contains it **and** a maintainer added the `preview:enabled` label to that PR. -`/pr/:number` follows the same rule: same-repo PRs always render, fork PRs only with the `preview:enabled` label. Removing the label revokes both within about a minute (the decision cache's TTL). +`/pr/:number` follows the same rule: same-repo PRs always render, fork PRs only with the `preview:enabled` label. Removing the label revokes both within 10 minutes (the decision cache's TTL). Every other SHA answers 404, and `/tree/` rejects GitHub's hidden `pull//head` refs, so the label check can't be sidestepped through a branch preview. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03ca933..2601cde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,23 +12,29 @@ importers: .: dependencies: '@ai-sdk/gateway': - specifier: ^4.0.62 - version: 4.0.62(zod@4.4.3) + specifier: ^4.0.78 + version: 4.0.78(zod@4.6.1) '@ai-sdk/vue': - specifier: ^4.0.77 - version: 4.0.77(vue@3.5.41(typescript@6.0.3))(zod@4.4.3) + specifier: ^4.0.97 + version: 4.0.97(vue@3.5.41(typescript@6.0.3))(zod@4.6.1) '@comark/nuxt': specifier: ^0.6.2 - version: 0.6.2(a7af4bed794ccaa8b491cb4661979ebb) + version: 0.6.2(d45826d7a399b3ded44dd0ff86d4ffd7) + '@iconify-json/logos': + specifier: ^1.2.14 + version: 1.2.14 '@iconify-json/lucide': - specifier: ^1.2.125 - version: 1.2.125 + specifier: ^1.2.131 + version: 1.2.131 '@iconify-json/simple-icons': - specifier: ^1.2.93 - version: 1.2.93 + specifier: ^1.2.95 + version: 1.2.95 + '@iconify-json/unjs': + specifier: ^1.2.4 + version: 1.2.4 '@iconify-json/vscode-icons': - specifier: ^1.2.74 - version: 1.2.74 + specifier: ^1.2.77 + version: 1.2.77 '@iconify/vue': specifier: ^5.0.1 version: 5.0.1(vue@3.5.41(typescript@6.0.3)) @@ -37,16 +43,16 @@ importers: version: 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) '@nuxt/ui': specifier: ^4.11.1 - version: 4.11.1(0716ad8e1ed9af83500c5a16dcdaaa58) + version: 4.11.1(f9711837b53e5eed9eacf00ad56e4c40) '@nuxtjs/mcp-toolkit': specifier: ^0.18.1 - version: 0.18.1(@vue/compiler-sfc@3.5.41)(h3@1.15.11)(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(zod@4.4.3) + version: 0.18.1(@vue/compiler-sfc@3.5.41)(h3@1.15.11)(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(zod@4.6.1) '@nuxtjs/robots': specifier: ^6.2.0 - version: 6.2.0(3bc4744619a624c5169bc56293f3c985) + version: 6.2.0(9963a669bf37ac17e292d4f6f824546f) '@nuxtjs/sitemap': specifier: ^8.5.0 - version: 8.5.0(3bc4744619a624c5169bc56293f3c985) + version: 8.5.0(9963a669bf37ac17e292d4f6f824546f) '@octokit/webhooks-methods': specifier: ^6.0.0 version: 6.0.0 @@ -56,27 +62,30 @@ importers: '@resvg/resvg-js': specifier: ^2.6.2 version: 2.6.2 + '@sqlite.org/sqlite-wasm': + specifier: 3.53.0-build1 + version: 3.53.0-build1 '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + version: 2.0.1(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) '@vercel/functions': - specifier: ^3.9.5 - version: 3.9.5(ws@8.21.3) + specifier: ^3.9.7 + version: 3.9.7(ws@8.21.3) '@vercel/global-config': specifier: ^1.5.1 version: 1.5.1(@opentelemetry/api@1.9.1) '@vercel/otel': specifier: ^2.1.3 - version: 2.1.3(@opentelemetry/api-logs@0.221.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.221.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + version: 2.1.3(@opentelemetry/api-logs@0.221.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.221.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)) '@vercel/speed-insights': specifier: ^2.0.0 - version: 2.0.0(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + version: 2.0.0(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) '@vueuse/core': specifier: ^14.4.0 version: 14.4.0(vue@3.5.41(typescript@6.0.3)) ai: - specifier: ^7.0.77 - version: 7.0.77(zod@4.4.3) + specifier: ^7.0.97 + version: 7.0.97(zod@4.6.1) beautiful-mermaid: specifier: ^1.1.3 version: 1.1.3 @@ -84,8 +93,8 @@ importers: specifier: ^0.6.2 version: 0.6.2(beautiful-mermaid@1.1.3)(rangi@2.2.0)(shiki@4.4.3) comark-content: - specifier: https://pkg.pr.new/comark-content@baefd4d - version: https://pkg.pr.new/comark-content@baefd4d(@vercel/functions@3.9.5(ws@8.21.3))(beautiful-mermaid@1.1.3)(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(rangi@2.2.0)(shiki@4.4.3) + specifier: ^0.4.0 + version: 0.4.0(@vercel/functions@3.9.7(ws@8.21.3))(beautiful-mermaid@1.1.3)(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(rangi@2.2.0)(shiki@4.4.3) defu: specifier: ^6.1.7 version: 6.1.7 @@ -93,26 +102,29 @@ importers: specifier: ^1.1.1 version: 1.1.1 js-yaml: - specifier: ^5.3.0 - version: 5.3.0 + specifier: ^5.4.1 + version: 5.4.1 motion-v: - specifier: ^2.4.0 + specifier: ^2.4.2 version: 2.4.2(@vueuse/core@14.4.0(vue@3.5.41(typescript@6.0.3)))(vue@3.5.41(typescript@6.0.3)) nuxt-agent-discovery: specifier: ^0.5.1 - version: 0.5.1(3adf33016b78b1d9f3d0bf8940d45a28) + version: 0.5.1(93fae588a377ff5ba3fd984718cb8b37) nuxt-llms: specifier: https://pkg.pr.new/nuxt-content/nuxt-llms/nuxt-llms@f6a9730 version: https://pkg.pr.new/nuxt-content/nuxt-llms/nuxt-llms@f6a9730(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) nuxt-og-image: specifier: ^6.7.8 - version: 6.7.8(d69daeae2668f4062a5626b724092832) + version: 6.7.8(02a9fd06dc986c7b60d00a5f031b9e09) nuxt-schema-org: specifier: ^6.3.1 - version: 6.3.1(922ee4371f2ec5825b5f186a85055e54) + version: 6.3.1(374ede1556afd53e77a90e7d78db3c9d) nuxt-seo-utils: - specifier: ^8.4.2 - version: 8.4.2(6c2732a7424285fd39f1e74aab3b66f7) + specifier: ^8.5.0 + version: 8.5.0(0e3d028a172b021df1bc33f9e04f4227) + nuxt-workers: + specifier: ^0.1.0 + version: 0.1.0(magicast@0.5.4) pathe: specifier: ^2.0.3 version: 2.0.3 @@ -130,35 +142,35 @@ importers: version: 1.6.4 unstorage: specifier: ^1.17.5 - version: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + version: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: ^4.6.1 + version: 4.6.1 devDependencies: '@nuxt/devtools-kit': specifier: 4.0.0-alpha.9 version: 4.0.0-alpha.9(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) '@nuxt/eslint-config': specifier: ^1.17.0 - version: 1.17.0(@typescript-eslint/utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + version: 1.17.0(@typescript-eslint/utils@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@opentelemetry/exporter-trace-otlp-proto': specifier: ^0.221.0 version: 0.221.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': - specifier: ^2.10.0 - version: 2.10.0(@opentelemetry/api@1.9.1) + specifier: ^2.11.0 + version: 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': - specifier: ^2.10.0 - version: 2.10.0(@opentelemetry/api@1.9.1) + specifier: ^2.11.0 + version: 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-node': - specifier: ^2.10.0 - version: 2.10.0(@opentelemetry/api@1.9.1) + specifier: ^2.11.0 + version: 2.11.0(@opentelemetry/api@1.9.1) eslint: - specifier: ^10.9.0 - version: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + specifier: ^10.10.0 + version: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) nuxt: specifier: ^4.5.2 - version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -176,32 +188,32 @@ importers: version: link:.. nuxt: specifier: ^4.5.0 - version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) devDependencies: nuxtseo-layer-devtools: specifier: ^5.3.6 - version: 5.3.14(9f33b9b9e7e20aae9f2d2c74340473d1) + version: 5.3.14(ca9ba60081d8b5129f3ddf17535a8092) packages: - '@ai-sdk/gateway@4.0.62': - resolution: {integrity: sha512-zR3pustGWhw5eUZHG+fJZx/V/PBe+LxdDpc5hDFWxozG/3MB/+eY62jn+YiR+9uOH+Hx63e5zJoeKLfZfPktWQ==} + '@ai-sdk/gateway@4.0.78': + resolution: {integrity: sha512-fcamzmFlcy+c/tIc7QcATLe/kArgSVGQ1QxaddMjhOSX8zypc2hmD5sHB2bp1DSFcSxDD32d6usF9a7xLCTULg==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@5.0.29': - resolution: {integrity: sha512-7EIbwXiXKGa7EFk6tDZpuZBs6lxhEJpOuHeqrDb3Vd85uYdjwkdRuHnZDVDIIb2+QTSRmyph2NrXcbvuO/KAjQ==} + '@ai-sdk/provider-utils@5.0.39': + resolution: {integrity: sha512-VIFp4Qv3j+V941crFB1y2VG5yeGbeLOFRxiPaZJETkkpo1H5WviLx6H5yRkFkrXIGxnTGygKsPgCAJG0X61Auw==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@4.0.7': - resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==} + '@ai-sdk/provider@4.0.13': + resolution: {integrity: sha512-sJvnbFIFLzmKFXIjfwS8XCOAj6puHCawItwvA9PBZgk2f/l/TiC4OSfK3ueDbUVXfRCOn5eJN97EIF10toIOmQ==} engines: {node: '>=22'} - '@ai-sdk/vue@4.0.77': - resolution: {integrity: sha512-Yd6hjkUA+n0QtUgUr+aX0tquKNplp27r3W1vFshx4pYpJnFWuls5TAi4pzmPNZ2ssvM3QNIFhnruSr+q0nPnPg==} + '@ai-sdk/vue@4.0.97': + resolution: {integrity: sha512-QqtA+WhpI2eSArjqht0a3NplnZrNokKN5BszHxvJuWGfQxkSFiJFRbky9MwvNQDopiafc6nGK0l62PW5AsgOsQ==} engines: {node: '>=22'} peerDependencies: vue: ^3.3.4 @@ -369,6 +381,12 @@ packages: commander: optional: true + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@capsizecss/unpack@4.0.1': resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} engines: {node: '>=18'} @@ -798,8 +816,8 @@ packages: resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.7.2': - resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@fingerprintjs/botd@2.0.0': @@ -846,14 +864,20 @@ packages: '@iconify-json/carbon@1.2.25': resolution: {integrity: sha512-7GLgXnmi47Skh8DcPPiP8U3vgm3rOVziEe+flCdG/kwiVaeb649U3Eqt/ej4f3NvlZK8BrOxpR3xQlWFLZYblQ==} - '@iconify-json/lucide@1.2.125': - resolution: {integrity: sha512-tOCk1QKMtKnCfPAgZRHgjRkQTP7wF5IO+iPKvvp8vxGZYPkSLhx4HTV3Ng0pIZ3wNWrS6kVpHkunJ1dc19L1og==} + '@iconify-json/logos@1.2.14': + resolution: {integrity: sha512-O36DicXkgAMT6NAsH7MTWlOql8NktHz9fBCItTjz6L/9gyRXw4vQj6qTeVgww5UZWiVugUV1MhVFA/g9HWIkBw==} + + '@iconify-json/lucide@1.2.131': + resolution: {integrity: sha512-2h0kGA4utt2OIqTdDHPs7+ft0lK8fRRNx1ItsvUaIlkTKaNc/ADT3F9j2F18Qevm9sWfKlAghOUmxmP32inTPg==} + + '@iconify-json/simple-icons@1.2.95': + resolution: {integrity: sha512-QwWgcoiL+eNCD38QM0OStFVFoOgDvzeHrcwMAvATvcFl0VvMEtED92qp3K3juS0H6RZ9BGUlgq9mODPwNWkjJg==} - '@iconify-json/simple-icons@1.2.93': - resolution: {integrity: sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==} + '@iconify-json/unjs@1.2.4': + resolution: {integrity: sha512-ueSrChjHps8u6jTSDYTAp+4OQ/k+E11PbKZQjKkDVnOxNpC+/fUXXkYgrnqEUgT0rSvUiS0B7X5A8GcsP/PjOA==} - '@iconify-json/vscode-icons@1.2.74': - resolution: {integrity: sha512-ZVf1IM5sOvvY+0Gc6jtuD5okkd51mJe/BoVMhWJCu1WqZLSF6XgbSguxUOyuBmMjJXNPSw1OBvPwEVq1bXfiTw==} + '@iconify-json/vscode-icons@1.2.77': + resolution: {integrity: sha512-yl8e3KlFfLLhiL9CH6YwX6syePeSLLwH2dL4cKGYC27SgdNHL3TRpheAyOjO3fw2+Ovp3tOSF5oj3AhSv4MQ8Q==} '@iconify/collections@1.0.727': resolution: {integrity: sha512-n+ipAqz5A6TWExnndwdT6qVX7KfXiw5GNDSxO2otXs6rxFWdHqizHTW1WvmWmT6m2BqQ8lhzUyWSJvm4M3nMAg==} @@ -873,160 +897,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.3': - resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.3': - resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.3': - resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.2': - resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.2': - resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.3': - resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.3': - resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.3': - resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.3': - resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.3': - resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.3': - resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.3': - resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.3': - resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.3': - resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.3': - resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.3': - resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -1067,6 +1091,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -1179,6 +1212,10 @@ packages: '@nuxt/icon@2.5.1': resolution: {integrity: sha512-zBP72Po7BS+tXzoeDRA/Y9TTY77OIcNCyzfgXsOmep7zZShTEoe4p1WZBXce/9oJDK3YXmbYC3ILQ7XvqM4/XA==} + '@nuxt/kit@3.21.11': + resolution: {integrity: sha512-0Xi3tgwN77w43Q8GCPIrvWmF1J7Peehkts44E0uKNIml9lB8WoUn8YxyUjxBv47XtVR86NWoWALAT+/IEMHJEA==} + engines: {node: '>=18.12.0'} + '@nuxt/kit@4.5.2': resolution: {integrity: sha512-l66LU9DcJYjmNwqwAj2I5UGRrUbnG2DOKGChnN70zIGtn0eq/z87gi/FRgha6eMb9/FmB1PFHgtx6PWVml1C2Q==} engines: {node: '>=18.12.0'} @@ -1210,6 +1247,65 @@ packages: peerDependencies: '@nuxt/kit': '>=3.0.0' + '@nuxt/ui@4.11.0': + resolution: {integrity: sha512-gDs67/fWk3kipcEV4Pc5h1VnZD1glfWfINmJU7s3qpkxxRTkfkPxnUszXG664whNWcoqucS1WgZ/rNjZa3pTMw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@inertiajs/vue3': ^2.0.7 || ^3.0.0 + '@internationalized/date': ^3.0.0 + '@internationalized/number': ^3.0.0 + '@nuxt/content': ^3.0.0 + '@tiptap/core': ^3 + '@tiptap/extension-bubble-menu': ^3 + '@tiptap/extension-code': ^3 + '@tiptap/extension-collaboration': ^3 + '@tiptap/extension-drag-handle': ^3 + '@tiptap/extension-drag-handle-vue-3': ^3 + '@tiptap/extension-floating-menu': ^3 + '@tiptap/extension-horizontal-rule': ^3 + '@tiptap/extension-image': ^3 + '@tiptap/extension-mention': ^3 + '@tiptap/extension-node-range': ^3 + '@tiptap/extension-placeholder': ^3 + '@tiptap/markdown': ^3 + '@tiptap/pm': ^3 + '@tiptap/starter-kit': ^3 + '@tiptap/suggestion': ^3 + '@tiptap/vue-3': ^3 + ai: ^6 || ^7 + joi: ^18.0.0 + superstruct: ^2.0.0 + tailwindcss: ^4.0.0 + typescript: ^5.6.3 || ^6.0.0 || ^7.0.0 + valibot: ^1.0.0 + vue-router: ^4.5.0 || ^5.0.0 + yup: ^1.7.0 + zod: ^3.24.0 || ^4.0.0 + peerDependenciesMeta: + '@inertiajs/vue3': + optional: true + '@internationalized/date': + optional: true + '@internationalized/number': + optional: true + '@nuxt/content': + optional: true + ai: + optional: true + joi: + optional: true + superstruct: + optional: true + valibot: + optional: true + vue-router: + optional: true + yup: + optional: true + zod: + optional: true + '@nuxt/ui@4.11.1': resolution: {integrity: sha512-6/xTMQNO4bcVesaaiIorH65cZ3eu0xenH+gC2L/b9pfmeVc0aMGXDKpW2JxoVUlrlcHo/beoUTI0B164HcwweQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1343,8 +1439,8 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.10.0': - resolution: {integrity: sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==} + '@opentelemetry/context-async-hooks@2.11.0': + resolution: {integrity: sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1355,6 +1451,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.11.0': + resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/exporter-trace-otlp-proto@0.221.0': resolution: {integrity: sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1385,6 +1487,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.11.0': + resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-logs@0.221.0': resolution: {integrity: sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1397,14 +1505,14 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.10.0': - resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} + '@opentelemetry/sdk-trace-base@2.11.0': + resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.10.0': - resolution: {integrity: sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==} + '@opentelemetry/sdk-trace-node@2.11.0': + resolution: {integrity: sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1415,6 +1523,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace@2.11.0': + resolution: {integrity: sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.43.0': resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} @@ -2015,6 +2129,10 @@ packages: '@speed-highlight/core@1.2.24': resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + '@sqlite.org/sqlite-wasm@3.53.0-build1': + resolution: {integrity: sha512-PfWPWN2n+/37doa8oh2/oUXk4OOsRYZsxc1W1sDXIGb/Pu5Yrb+f2eyYpgQMGITVX7HVgxhs9P18Rc6I97ym/g==} + engines: {node: '>=22'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2662,15 +2780,15 @@ packages: vue-router: optional: true - '@vercel/cli-config@0.2.4': - resolution: {integrity: sha512-kZ5SojbrV06GHoU6QIWGwDXLov+s9rWZ7QqdqKfJfBGCNUieGfgaCjeeenNy8Y+QC0bwC0dZ2B4l5Hvdmrgpdw==} + '@vercel/cli-config@0.2.6': + resolution: {integrity: sha512-2AsKCf6gE/Eniq09ARm1RK9rQMV5pcAHU7BFcR9MISO+4/RsjjkaYbQN6G85/xi9pYZ5CJNXvYn4TI6p1jKBcg==} '@vercel/cli-exec@1.0.1': resolution: {integrity: sha512-g9XerViJ/paZujufXYcu5XYI2vU2rtB4sgdpjUHde5RnOkdmpu0ngH46LCFGHoPXO/C+qDPSczIHIRN+8Q2YKQ==} engines: {node: '>= 18'} - '@vercel/functions@3.9.5': - resolution: {integrity: sha512-EUfqlb7AzoEh7URlMNAO4jbJiLWz9grDBHvfjKTDvEP9c8y3DqX3SWPvfaQkUjtkm3b83flhaUUMuewdHa+qmw==} + '@vercel/functions@3.9.7': + resolution: {integrity: sha512-ICLzHbVZs10igkVMW/ovEMIXFa5ctpEjTa+VWRCOQbIqg9ALZcQcRjcfDuQZ/g2XtPS08EvHjzm538YJU9WsXA==} engines: {node: '>= 20'} peerDependencies: '@aws-sdk/credential-provider-web-identity': '*' @@ -2705,8 +2823,8 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} - '@vercel/oidc@3.8.5': - resolution: {integrity: sha512-RwXYtnt6za+5UO4IaLywN/6B95AlLqynPRUWRJxeJ/qufwkcLUbZNUxYtzT0uMpuraWhlNcGqPNGkTnZr4BGBw==} + '@vercel/oidc@3.8.7': + resolution: {integrity: sha512-fRu59npOu+1vsV570tiLKskfuRyOJ1MqceAkXbTIfWPnM+c9ve1tSK81oj4umKKirymlGUss3V60Lm5I38rKDw==} engines: {node: '>= 20'} '@vercel/otel@2.1.3': @@ -2977,8 +3095,8 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - ai@7.0.77: - resolution: {integrity: sha512-muLtBSTAUCreR77L16w4AFBiX2gK/RNt84EKp8m03SN9+MfNlC5EGqYYttRjYKV3xe0a33yj1Zawj1EnjejIWw==} + ai@7.0.97: + resolution: {integrity: sha512-tQGZ234p/bk5X/LNQdB43a5/z1Bve7wK0EOR1AwN6xrosZih4MIC/3pXhcnytcpBJHMbhLISW6J1dFTd/tnwPA==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -3204,6 +3322,9 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -3281,9 +3402,8 @@ packages: colortranslator@5.0.0: resolution: {integrity: sha512-Z3UPUKasUVDFCDYAjP2fmlVRf1jFHJv1izAmPjiOa0OCIw1W7iC8PZ2GsoDa8uZv+mKyWopxxStT9q05+27h7w==} - comark-content@https://pkg.pr.new/comark-content@baefd4d: - resolution: {integrity: sha512-7E/3OIIKPBI5XJ3s/aEtptAP49Of9W/EQVjzsbGnAZgCxvtlpqeSq1l+8GUbMDYqNy20rLWXlk7JdXTd8rsj/g==, tarball: https://pkg.pr.new/comark-content@baefd4d} - version: 0.3.0 + comark-content@0.4.0: + resolution: {integrity: sha512-wUCFpDdEl9tNXcAFVMkqpQL+Aa5hcg1YL5X8r+5oAohQkofRp+We4puz6QP4XR33MPbasA41pPfIVf4j+WrT4Q==} hasBin: true comark@0.6.2: @@ -3842,8 +3962,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.9.0: - resolution: {integrity: sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==} + eslint@10.10.0: + resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -3984,9 +4104,8 @@ packages: fflate@0.7.5: resolution: {integrity: sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g==} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -4007,9 +4126,8 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} @@ -4167,6 +4285,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -4194,6 +4316,12 @@ packages: hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} @@ -4404,8 +4532,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@5.3.0: - resolution: {integrity: sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==} + js-yaml@5.4.1: + resolution: {integrity: sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==} hasBin: true jsdoc-type-pratt-parser@8.0.0: @@ -4421,9 +4549,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4444,8 +4569,8 @@ packages: engines: {node: '>=6'} hasBin: true - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} @@ -5004,8 +5129,8 @@ packages: zod: optional: true - nuxt-seo-utils@8.4.2: - resolution: {integrity: sha512-l+UULXKMMuL5MC779k/lH1Rtv2nNbh5qWCBMEQp61LXo1HrMFfXoP6BdOAUj+dcAXhEdOAVZIXWH2qiBM3oAjg==} + nuxt-seo-utils@8.5.0: + resolution: {integrity: sha512-vq5rP7xBLEYGUBskzllqg0tmRTt8EyhzX1tuvWEGRt4JKAhw2HN75GUP1RpAM4n6QYqHyh6U8YkJZjEuot0wpQ==} hasBin: true peerDependencies: '@unhead/vue': ^2.0.7 || ^3.0.0 @@ -5034,6 +5159,9 @@ packages: peerDependencies: vue: ^3.5.30 + nuxt-workers@0.1.0: + resolution: {integrity: sha512-npsxy72FRQZkxHV1Y+KCkuSvb2Y/7Tcp7xGXPHXXVQ5/oIZ5+69VAudfdhpuwPN1JZJn9ULr01vKRLamENsTew==} + nuxt@4.5.2: resolution: {integrity: sha512-tR3fcqeHlHmmkLMpIg3V7Y+1ltr302lW8djMw/iy+myfo7QSSz+BVJDuQhg5j73b9oteSyBfOKTDYTgvMtj6TA==} engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} @@ -5510,6 +5638,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + qs@6.15.3: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} @@ -5589,6 +5721,11 @@ packages: resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} hasBin: true + reka-ui@2.10.3: + resolution: {integrity: sha512-nJGZbwcha8AcP2wbnjodfzsciTKBqp1mIzepC9Pi2xDI/YK3++ej3vpJOSPyxqrkq1Oorj4K+BdJkbVCV6x6ag==} + peerDependencies: + vue: '>= 3.4.0' + reka-ui@2.10.4: resolution: {integrity: sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw==} peerDependencies: @@ -5730,8 +5867,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.35.3: - resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -6718,38 +6855,38 @@ packages: zod@4.1.11: resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.1: + resolution: {integrity: sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: - '@ai-sdk/gateway@4.0.62(zod@4.4.3)': + '@ai-sdk/gateway@4.0.78(zod@4.6.1)': dependencies: - '@ai-sdk/provider': 4.0.7 - '@ai-sdk/provider-utils': 5.0.29(zod@4.4.3) + '@ai-sdk/provider': 4.0.13 + '@ai-sdk/provider-utils': 5.0.39(zod@4.6.1) '@vercel/oidc': 3.2.0 - zod: 4.4.3 + zod: 4.6.1 - '@ai-sdk/provider-utils@5.0.29(zod@4.4.3)': + '@ai-sdk/provider-utils@5.0.39(zod@4.6.1)': dependencies: - '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider': 4.0.13 '@standard-schema/spec': 1.1.0 '@workflow/serde': 4.1.0 eventsource-parser: 3.1.1 undici: 7.29.0 - zod: 4.4.3 + zod: 4.6.1 - '@ai-sdk/provider@4.0.7': + '@ai-sdk/provider@4.0.13': dependencies: json-schema: 0.4.0 - '@ai-sdk/vue@4.0.77(vue@3.5.41(typescript@6.0.3))(zod@4.4.3)': + '@ai-sdk/vue@4.0.97(vue@3.5.41(typescript@6.0.3))(zod@4.6.1)': dependencies: - '@ai-sdk/provider-utils': 5.0.29(zod@4.4.3) - ai: 7.0.77(zod@4.4.3) + '@ai-sdk/provider-utils': 5.0.39(zod@4.6.1) + ai: 7.0.97(zod@4.6.1) swrv: 1.2.0(vue@3.5.41(typescript@6.0.3)) vue: 3.5.41(typescript@6.0.3) transitivePeerDependencies: @@ -6960,6 +7097,18 @@ snapshots: optionalDependencies: citty: 0.2.2 + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + '@capsizecss/unpack@4.0.1': dependencies: fontkitten: 1.0.3 @@ -6980,12 +7129,12 @@ snapshots: '@colordx/core@5.6.0': {} - '@comark/nuxt@0.6.2(a7af4bed794ccaa8b491cb4661979ebb)': + '@comark/nuxt@0.6.2(d45826d7a399b3ded44dd0ff86d4ffd7)': dependencies: '@comark/vue': 0.6.2(beautiful-mermaid@1.1.3)(rangi@2.2.0)(shiki@4.4.3)(vue@3.5.41(typescript@6.0.3)) '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) comark: 0.6.2(beautiful-mermaid@1.1.3)(rangi@2.2.0)(shiki@4.4.3) - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) transitivePeerDependencies: - beautiful-mermaid - katex @@ -7221,18 +7370,18 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.1.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))': + '@eslint/compat@2.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: '@eslint/core': 1.2.1 optionalDependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) '@eslint/config-array@0.23.5(supports-color@10.2.2)': dependencies: @@ -7259,13 +7408,13 @@ snapshots: mdn-data: 2.29.0 source-map-js: 1.2.1 - '@eslint/js@10.0.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))': + '@eslint/js@10.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': optionalDependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.7.2': + '@eslint/plugin-kit@0.7.3': dependencies: '@eslint/core': 1.2.1 levn: 0.4.1 @@ -7316,15 +7465,23 @@ snapshots: dependencies: '@iconify/types': 2.0.0 - '@iconify-json/lucide@1.2.125': + '@iconify-json/logos@1.2.14': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/lucide@1.2.131': dependencies: '@iconify/types': 2.0.0 - '@iconify-json/simple-icons@1.2.93': + '@iconify-json/simple-icons@1.2.95': dependencies: '@iconify/types': 2.0.0 - '@iconify-json/vscode-icons@1.2.74': + '@iconify-json/unjs@1.2.4': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/vscode-icons@1.2.77': dependencies: '@iconify/types': 2.0.0 @@ -7348,108 +7505,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.3': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.3': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.3': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.2': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.2': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.2': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.2': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.2': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.2': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.2': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.2': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.2': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.3': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.3': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.3': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.3': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.3': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.3': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.3': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.3': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.3': + '@img/sharp-wasm32@0.35.4': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.3': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.3': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.3': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.3': + '@img/sharp-win32-x64@0.35.4': optional: true '@internationalized/date@3.12.3': @@ -7499,6 +7656,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + '@kwsites/file-exists@1.1.1(supports-color@10.2.2)': dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -7520,7 +7685,7 @@ snapshots: - encoding - supports-color - '@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.6.1)': dependencies: '@hono/node-server': 2.1.1(hono@4.13.4) ajv: 8.20.0 @@ -7537,8 +7702,8 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) + zod: 4.6.1 + zod-to-json-schema: 3.25.2(zod@4.6.1) transitivePeerDependencies: - supports-color @@ -7655,7 +7820,7 @@ snapshots: pkg-types: 2.3.1 semver: 7.8.5 - '@nuxt/devtools@3.4.2(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(oxc-parser@0.143.0)(rolldown@1.2.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + '@nuxt/devtools@3.4.2(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(oxc-parser@0.143.0)(rolldown@1.2.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': dependencies: '@nuxt/devtools-kit': 3.4.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) '@nuxt/devtools-wizard': 3.4.2 @@ -7685,7 +7850,7 @@ snapshots: sirv: 3.0.2 structured-clone-es: 2.0.1 tinyglobby: 0.2.17 - unstorage: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + unstorage: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) vite-plugin-inspect: 11.4.1(@nuxt/kit@4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) vite-plugin-vue-tracer: 1.5.0(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) @@ -7720,30 +7885,30 @@ snapshots: - utf-8-validate - vue - '@nuxt/eslint-config@1.17.0(@typescript-eslint/utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@nuxt/eslint-config@1.17.0(@typescript-eslint/utils@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@antfu/install-pkg': 2.0.1 '@clack/prompts': 1.7.0 - '@eslint/js': 10.0.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - '@nuxt/eslint-plugin': 1.17.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@stylistic/eslint-plugin': 5.10.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-config-flat-gitignore: 2.3.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint/js': 10.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + '@nuxt/eslint-plugin': 1.17.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) + eslint-config-flat-gitignore: 2.3.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) eslint-flat-config-utils: 3.2.0 - eslint-merge-processors: 2.0.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-import-lite: 0.6.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) - eslint-plugin-jsdoc: 63.3.3(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) - eslint-plugin-regexp: 3.2.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-unicorn: 73.0.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-vue: 10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)) - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-merge-processors: 2.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-import-lite: 0.6.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-plugin-jsdoc: 63.3.3(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-plugin-regexp: 3.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-unicorn: 73.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-vue: 10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) globals: 17.11.0 local-pkg: 1.2.1 pathe: 2.0.3 - vue-eslint-parser: 10.4.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + vue-eslint-parser: 10.4.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) transitivePeerDependencies: - '@typescript-eslint/utils' - '@vue/compiler-sfc' @@ -7751,22 +7916,22 @@ snapshots: - supports-color - typescript - '@nuxt/eslint-plugin@1.17.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@nuxt/eslint-plugin@1.17.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/utils': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) transitivePeerDependencies: - supports-color - typescript - '@nuxt/fonts@0.14.0(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': + '@nuxt/fonts@0.14.0(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': dependencies: '@nuxt/devtools-kit': 3.4.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) consola: 3.4.2 defu: 6.1.7 - fontless: 0.2.1(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + fontless: 0.2.1(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) h3: 1.15.11 magic-regexp: 0.10.0 ofetch: 1.5.1 @@ -7776,7 +7941,7 @@ snapshots: ufo: 1.6.4 unifont: 0.7.5 unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) - unstorage: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + unstorage: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7835,6 +8000,32 @@ snapshots: - vite - vue + '@nuxt/kit@3.21.11(magicast@0.5.4)': + dependencies: + c12: 3.3.4(magicast@0.5.4) + consola: 3.4.2 + defu: 6.1.7 + destr: 2.0.5 + errx: 0.1.2 + exsolve: 1.1.1 + ignore: 7.0.6 + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + mlly: 1.8.2 + ohash: 2.0.12 + pathe: 2.0.3 + pkg-types: 2.3.1 + rc9: 3.0.1 + scule: 1.3.0 + semver: 7.8.5 + tinyglobby: 0.2.17 + ufo: 1.6.4 + unctx: 2.5.0 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + '@nuxt/kit@4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))': dependencies: c12: 3.3.4(magicast@0.5.4) @@ -7865,7 +8056,7 @@ snapshots: - rolldown - unplugin - '@nuxt/nitro-server@4.5.2(80fa4b35290e6e68e37c82a4f05aec13)': + '@nuxt/nitro-server@4.5.2(188bc3c2c4363fd52825c16fd4cbef34)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) @@ -7882,9 +8073,9 @@ snapshots: impound: 1.1.7(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.13.4(@vercel/functions@3.9.5(ws@8.21.3))(oxc-parser@0.143.0)(rolldown@1.2.5)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + nitropack: 2.13.4(@vercel/functions@3.9.7(ws@8.21.3))(oxc-parser@0.143.0)(rolldown@1.2.5)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) nostics: 1.2.0 - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) nypm: 0.6.9 ohash: 2.0.12 pathe: 2.0.3 @@ -7892,7 +8083,7 @@ snapshots: std-env: 4.2.0 ufo: 1.6.4 unctx: 3.0.1(magic-string@1.2.3)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) - unstorage: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + unstorage: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) vue: 3.5.41(typescript@6.0.3) vue-bundle-renderer: 2.3.2 vue-devtools-stub: 0.1.0 @@ -7969,11 +8160,134 @@ snapshots: rc9: 3.0.1 std-env: 4.2.0 - '@nuxt/ui@4.11.1(0716ad8e1ed9af83500c5a16dcdaaa58)': + '@nuxt/ui@4.11.0(4df280129450d67e9f51cc0909bcdbf1)': dependencies: '@floating-ui/dom': 1.8.0 '@iconify/vue': 5.0.1(vue@3.5.41(typescript@6.0.3)) - '@nuxt/fonts': 0.14.0(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + '@nuxt/fonts': 0.14.0(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + '@nuxt/icon': 2.5.1(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) + '@nuxt/schema': 4.5.2 + '@nuxtjs/color-mode': 4.0.1(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) + '@standard-schema/spec': 1.1.0 + '@tailwindcss/postcss': 4.3.3 + '@tailwindcss/vite': 4.3.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + '@tanstack/vue-table': 8.21.3(vue@3.5.41(typescript@6.0.3)) + '@tanstack/vue-virtual': 3.13.36(vue@3.5.41(typescript@6.0.3)) + '@tiptap/core': 3.30.3(@tiptap/pm@3.30.3) + '@tiptap/extension-bubble-menu': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3) + '@tiptap/extension-code': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3)) + '@tiptap/extension-collaboration': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32) + '@tiptap/extension-drag-handle': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/extension-collaboration@3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32))(@tiptap/extension-node-range@3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32)) + '@tiptap/extension-drag-handle-vue-3': 3.30.3(@tiptap/extension-drag-handle@3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/extension-collaboration@3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32))(@tiptap/extension-node-range@3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32)))(@tiptap/pm@3.30.3)(@tiptap/vue-3@3.30.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(vue@3.5.41(typescript@6.0.3)))(vue@3.5.41(typescript@6.0.3)) + '@tiptap/extension-floating-menu': 3.30.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3) + '@tiptap/extension-horizontal-rule': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3) + '@tiptap/extension-image': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3)) + '@tiptap/extension-mention': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(@tiptap/suggestion@3.30.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)) + '@tiptap/extension-node-range': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3) + '@tiptap/extension-placeholder': 3.30.3(@tiptap/extensions@3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)) + '@tiptap/markdown': 3.30.3(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3) + '@tiptap/pm': 3.30.3 + '@tiptap/starter-kit': 3.30.3 + '@tiptap/suggestion': 3.30.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3) + '@tiptap/vue-3': 3.30.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.3(@tiptap/pm@3.30.3))(@tiptap/pm@3.30.3)(vue@3.5.41(typescript@6.0.3)) + '@unhead/vue': 3.4.0(@oxc-project/types@0.146.0)(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@vueuse/core': 14.4.0(vue@3.5.41(typescript@6.0.3)) + '@vueuse/integrations': 14.4.0(change-case@5.4.4)(fuse.js@7.5.0)(vue@3.5.41(typescript@6.0.3)) + '@vueuse/shared': 14.4.0(vue@3.5.41(typescript@6.0.3)) + colortranslator: 5.0.0 + consola: 3.4.2 + defu: 6.1.7 + embla-carousel-auto-height: 8.6.0(embla-carousel@8.6.0) + embla-carousel-auto-scroll: 8.6.0(embla-carousel@8.6.0) + embla-carousel-autoplay: 8.6.0(embla-carousel@8.6.0) + embla-carousel-class-names: 8.6.0(embla-carousel@8.6.0) + embla-carousel-fade: 8.6.0(embla-carousel@8.6.0) + embla-carousel-vue: 8.6.0(vue@3.5.41(typescript@6.0.3)) + embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0) + fuse.js: 7.5.0 + hookable: 6.1.1 + knitwork: 1.3.0 + magic-string: 1.2.3 + mlly: 1.8.2 + motion-v: 2.4.2(@vueuse/core@14.4.0(vue@3.5.41(typescript@6.0.3)))(vue@3.5.41(typescript@6.0.3)) + ohash: 2.0.12 + pathe: 2.0.3 + reka-ui: 2.10.3(vue@3.5.41(typescript@6.0.3)) + scule: 1.3.0 + tailwind-merge: 3.6.0 + tailwind-variants: 3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + tailwindcss: 4.3.3 + tinyglobby: 0.2.17 + typescript: 6.0.3 + ufo: 1.6.4 + unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + unplugin-auto-import: 21.1.0(@nuxt/kit@4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))))(@vueuse/core@14.4.0(vue@3.5.41(typescript@6.0.3)))(esbuild@0.28.2)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + unplugin-vue-components: 32.1.0(@nuxt/kit@4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))))(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + vaul-vue: 0.4.1(reka-ui@2.10.3(vue@3.5.41(typescript@6.0.3)))(vue@3.5.41(typescript@6.0.3)) + vue-component-type-helpers: 3.3.11 + optionalDependencies: + '@internationalized/date': 3.12.3 + '@internationalized/number': 3.6.7 + ai: 7.0.97(zod@4.6.1) + vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + zod: 4.6.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@farmfe/core' + - '@netlify/blobs' + - '@oxc-project/types' + - '@planetscale/database' + - '@rspack/core' + - '@unhead/cli' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools-kit' + - '@vue/composition-api' + - async-validator + - aws4fetch + - axios + - bun-types-no-globals + - change-case + - db0 + - drauu + - embla-carousel + - esbuild + - focus-trap + - idb-keyval + - ioredis + - jwt-decode + - lightningcss + - magicast + - nprogress + - oxc-parser + - qrcode + - react + - react-dom + - rolldown + - rollup + - sortablejs + - universal-cookie + - unloader + - uploadthing + - vite + - vue + - webpack + + '@nuxt/ui@4.11.1(f9711837b53e5eed9eacf00ad56e4c40)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@iconify/vue': 5.0.1(vue@3.5.41(typescript@6.0.3)) + '@nuxt/fonts': 0.14.0(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) '@nuxt/icon': 2.5.1(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) '@nuxt/schema': 4.5.2 @@ -8038,9 +8352,8 @@ snapshots: optionalDependencies: '@internationalized/date': 3.12.3 '@internationalized/number': 3.6.7 - ai: 7.0.77(zod@4.4.3) - vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) - zod: 4.4.3 + ai: 7.0.97(zod@4.6.1) + zod: 4.6.1 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -8092,7 +8405,7 @@ snapshots: - vue - webpack - '@nuxt/vite-builder@4.5.2(5ea4d7f0831f7e722792e954b23a4f93)': + '@nuxt/vite-builder@4.5.2(26f4d21de89f1c50f9572a8528f34eef)': dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) '@vitejs/plugin-vue': 6.0.8(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) @@ -8110,7 +8423,7 @@ snapshots: knitwork: 1.3.0 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) nypm: 0.6.9 pathe: 2.0.3 pkg-types: 2.3.1 @@ -8123,7 +8436,7 @@ snapshots: unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) vite-node: 6.0.0(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) - vite-plugin-checker: 0.14.5(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3)) + vite-plugin-checker: 0.14.5(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3)) vue: 3.5.41(typescript@6.0.3) vue-bundle-renderer: 2.3.2 optionalDependencies: @@ -8175,15 +8488,15 @@ snapshots: - rolldown - unplugin - '@nuxtjs/mcp-toolkit@0.18.1(@vue/compiler-sfc@3.5.41)(h3@1.15.11)(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(zod@4.4.3)': + '@nuxtjs/mcp-toolkit@0.18.1(@vue/compiler-sfc@3.5.41)(h3@1.15.11)(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(zod@4.6.1)': dependencies: - '@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@4.6.1) '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) '@vitejs/plugin-vue': 6.0.8(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) h3: 1.15.11 tinyglobby: 0.2.17 vite-plugin-singlefile: 2.3.3(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) - zod: 4.4.3 + zod: 4.6.1 optionalDependencies: '@vue/compiler-sfc': 3.5.41 vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) @@ -8198,20 +8511,20 @@ snapshots: - supports-color - unplugin - '@nuxtjs/robots@6.2.0(3bc4744619a624c5169bc56293f3c985)': + '@nuxtjs/robots@6.2.0(9963a669bf37ac17e292d4f6f824546f)': dependencies: '@fingerprintjs/botd': 2.0.0 '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) consola: 3.4.2 defu: 6.1.7 h3: 1.15.11 - nuxt-site-config: 4.2.3(3bc4744619a624c5169bc56293f3c985) - nuxtseo-shared: 5.3.14(fc4f002cf8929f5932e9df9bf8f19792) + nuxt-site-config: 4.2.3(9963a669bf37ac17e292d4f6f824546f) + nuxtseo-shared: 5.3.14(0f8908bb880cd164227784aaff03d6a3) pathe: 2.0.3 pkg-types: 2.3.1 ufo: 1.6.4 optionalDependencies: - zod: 4.4.3 + zod: 4.6.1 transitivePeerDependencies: - '@nuxt/schema' - magic-string @@ -8223,13 +8536,13 @@ snapshots: - vite - vue - '@nuxtjs/sitemap@8.5.0(3bc4744619a624c5169bc56293f3c985)': + '@nuxtjs/sitemap@8.5.0(9963a669bf37ac17e292d4f6f824546f)': dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) consola: 3.4.2 defu: 6.1.7 - nuxt-site-config: 4.2.3(3bc4744619a624c5169bc56293f3c985) - nuxtseo-shared: 5.3.14(fc4f002cf8929f5932e9df9bf8f19792) + nuxt-site-config: 4.2.3(9963a669bf37ac17e292d4f6f824546f) + nuxtseo-shared: 5.3.14(0f8908bb880cd164227784aaff03d6a3) ofetch: 1.5.1 pathe: 2.0.3 pkg-types: 2.3.1 @@ -8238,7 +8551,7 @@ snapshots: ufo: 1.6.4 ultrahtml: 1.7.0 optionalDependencies: - zod: 4.4.3 + zod: 4.6.1 transitivePeerDependencies: - '@nuxt/schema' - magic-string @@ -8258,7 +8571,7 @@ snapshots: '@opentelemetry/api@1.9.1': {} - '@opentelemetry/context-async-hooks@2.10.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/context-async-hooks@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -8267,6 +8580,11 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -8305,6 +8623,12 @@ snapshots: '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -8319,20 +8643,20 @@ snapshots: '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-trace-node@2.10.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': dependencies: @@ -8341,6 +8665,13 @@ snapshots: '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/sdk-trace@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/semantic-conventions@1.43.0': {} '@oxc-parser/binding-android-arm-eabi@0.143.0': @@ -8721,13 +9052,15 @@ snapshots: '@speed-highlight/core@1.2.24': {} + '@sqlite.org/sqlite-wasm@3.53.0-build1': {} + '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@typescript-eslint/types': 8.67.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -9098,15 +9431,15 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/type-utils': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.67.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -9114,14 +9447,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/parser@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.67.0 '@typescript-eslint/types': 8.67.0 '@typescript-eslint/typescript-estree': 8.67.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.67.0 debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -9144,13 +9477,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.67.0 '@typescript-eslint/typescript-estree': 8.67.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -9173,13 +9506,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/utils@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.67.0 '@typescript-eslint/types': 8.67.0 '@typescript-eslint/typescript-estree': 8.67.0(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -9304,12 +9637,12 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vercel/analytics@2.0.1(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + '@vercel/analytics@2.0.1(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': optionalDependencies: - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) vue: 3.5.41(typescript@6.0.3) - '@vercel/cli-config@0.2.4': + '@vercel/cli-config@0.2.6': dependencies: xdg-app-paths: 5.5.1 zod: 4.1.11 @@ -9318,9 +9651,9 @@ snapshots: dependencies: execa: 5.1.1 - '@vercel/functions@3.9.5(ws@8.21.3)': + '@vercel/functions@3.9.7(ws@8.21.3)': dependencies: - '@vercel/oidc': 3.8.5 + '@vercel/oidc': 3.8.7 optionalDependencies: ws: 8.21.3 @@ -9353,25 +9686,25 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vercel/oidc@3.8.5': + '@vercel/oidc@3.8.7': dependencies: - '@vercel/cli-config': 0.2.4 + '@vercel/cli-config': 0.2.6 '@vercel/cli-exec': 1.0.1 jose: 5.10.0 - '@vercel/otel@2.1.3(@opentelemetry/api-logs@0.221.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.221.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + '@vercel/otel@2.1.3(@opentelemetry/api-logs@0.221.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.221.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.221.0 '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2) - '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) - '@vercel/speed-insights@2.0.0(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + '@vercel/speed-insights@2.0.0(nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': optionalDependencies: - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) vue: 3.5.41(typescript@6.0.3) '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': @@ -9599,13 +9932,13 @@ snapshots: '@vueuse/metadata@14.4.0': {} - '@vueuse/nuxt@14.4.0(fc4d6faa55285c3ac9a966d10279f90e)': + '@vueuse/nuxt@14.4.0(400f64eee5a6ea31a3859bc8b5e58b0e)': dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) '@vueuse/core': 14.4.0(vue@3.5.41(typescript@6.0.3)) '@vueuse/metadata': 14.4.0 local-pkg: 1.2.1 - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) vue: 3.5.41(typescript@6.0.3) transitivePeerDependencies: - magic-string @@ -9650,12 +9983,12 @@ snapshots: agent-base@7.1.4: {} - ai@7.0.77(zod@4.4.3): + ai@7.0.97(zod@4.6.1): dependencies: - '@ai-sdk/gateway': 4.0.62(zod@4.4.3) - '@ai-sdk/provider': 4.0.7 - '@ai-sdk/provider-utils': 5.0.29(zod@4.4.3) - zod: 4.4.3 + '@ai-sdk/gateway': 4.0.78(zod@4.6.1) + '@ai-sdk/provider': 4.0.13 + '@ai-sdk/provider-utils': 5.0.39(zod@4.6.1) + zod: 4.6.1 ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: @@ -9880,6 +10213,14 @@ snapshots: cac@7.0.0: {} + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -9950,7 +10291,7 @@ snapshots: colortranslator@5.0.0: {} - comark-content@https://pkg.pr.new/comark-content@baefd4d(@vercel/functions@3.9.5(ws@8.21.3))(beautiful-mermaid@1.1.3)(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(rangi@2.2.0)(shiki@4.4.3): + comark-content@0.4.0(@vercel/functions@3.9.7(ws@8.21.3))(beautiful-mermaid@1.1.3)(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(rangi@2.2.0)(shiki@4.4.3): dependencies: citty: 0.2.2 comark: 0.6.2(beautiful-mermaid@1.1.3)(rangi@2.2.0)(shiki@4.4.3) @@ -9961,7 +10302,7 @@ snapshots: picomatch: 4.0.5 slugify: 1.6.9 ufo: 1.6.4 - unstorage: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + unstorage: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9991,7 +10332,7 @@ snapshots: dependencies: entities: 8.0.0 htmlparser2: 12.0.0 - js-yaml: 5.3.0 + js-yaml: 5.4.1 markdown-exit: 1.1.0-beta.2 optionalDependencies: beautiful-mermaid: 1.1.3 @@ -10406,10 +10747,10 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-flat-gitignore@2.3.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-config-flat-gitignore@2.3.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - '@eslint/compat': 2.1.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + '@eslint/compat': 2.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-flat-config-utils@3.2.0: dependencies: @@ -10423,20 +10764,20 @@ snapshots: optionalDependencies: unrs-resolver: 1.12.2 - eslint-merge-processors@2.0.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-merge-processors@2.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-plugin-import-lite@0.6.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-import-lite@0.6.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@typescript-eslint/types': 8.67.0 comment-parser: 1.4.8 debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-context: 0.1.9(unrs-resolver@1.12.2) is-glob: 4.0.3 minimatch: 10.2.6 @@ -10444,11 +10785,11 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.12.2 optionalDependencies: - '@typescript-eslint/utils': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: - supports-color - eslint-plugin-jsdoc@63.3.3(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-jsdoc@63.3.3(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@es-joy/jsdoccomment': 0.91.0 '@es-joy/resolve.exports': 1.2.0 @@ -10456,7 +10797,7 @@ snapshots: comment-parser: 1.4.7 debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) espree: 11.2.0 esquery: 1.7.0 html-entities: 2.6.0 @@ -10468,20 +10809,20 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-regexp@3.2.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-regexp@3.2.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.8 - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) jsdoc-type-pratt-parser: 9.2.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-unicorn@73.0.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-unicorn@73.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@eslint/css-tree': 4.0.5 browserslist: 4.28.8 change-case: 5.4.4 @@ -10489,7 +10830,7 @@ snapshots: core-js-compat: 3.50.0 detect-indent: 7.0.2 entities: 4.5.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) find-up-simple: 1.0.1 globals: 17.11.0 indent-string: 5.0.0 @@ -10503,24 +10844,24 @@ snapshots: strip-indent: 4.1.1 yaml: 2.9.0 - eslint-plugin-vue@10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)): + eslint-plugin-vue@10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.5 semver: 7.8.5 - vue-eslint-parser: 10.4.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + vue-eslint-parser: 10.4.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) xml-name-validator: 5.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) - '@typescript-eslint/parser': 8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) + '@typescript-eslint/parser': 8.67.0(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: '@vue/compiler-sfc': 3.5.41 - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-scope@9.1.2: dependencies: @@ -10535,14 +10876,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2): + eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@10.2.2) '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.2 + '@eslint/plugin-kit': 0.7.3 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -10557,7 +10898,7 @@ snapshots: esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 + file-entry-cache: 11.1.5 find-up: 5.0.0 glob-parent: 6.0.2 ignore: 5.3.2 @@ -10745,9 +11086,9 @@ snapshots: fflate@0.7.5: {} - file-entry-cache@8.0.0: + file-entry-cache@11.1.5: dependencies: - flat-cache: 4.0.1 + flat-cache: 6.1.23 file-uri-to-path@1.0.0: {} @@ -10773,10 +11114,11 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@4.0.1: + flat-cache@6.1.23: dependencies: + cacheable: 2.5.0 flatted: 3.4.4 - keyv: 4.5.4 + hookified: 1.15.1 flatted@3.4.4: {} @@ -10796,7 +11138,7 @@ snapshots: dependencies: tiny-inflate: 1.0.3 - fontless@0.2.1(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)): + fontless@0.2.1(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)): dependencies: consola: 3.4.2 css-tree: 3.2.1 @@ -10810,7 +11152,7 @@ snapshots: pathe: 2.0.3 ufo: 1.6.4 unifont: 0.7.5 - unstorage: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + unstorage: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) optionalDependencies: vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) transitivePeerDependencies: @@ -10963,6 +11305,10 @@ snapshots: has-symbols@1.1.0: {} + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -10995,6 +11341,10 @@ snapshots: hookable@6.1.1: {} + hookified@1.15.1: {} + + hookified@2.2.0: {} + html-entities@2.6.0: {} html-void-elements@3.0.0: {} @@ -11183,7 +11533,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@5.3.0: + js-yaml@5.4.1: dependencies: argparse: 2.0.1 @@ -11195,8 +11545,6 @@ snapshots: jsesc@3.1.0: {} - json-buffer@3.0.1: {} - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -11209,9 +11557,9 @@ snapshots: json5@2.2.3: {} - keyv@4.5.4: + keyv@5.6.0: dependencies: - json-buffer: 3.0.1 + '@keyv/serialize': 1.1.1 kleur@4.1.5: {} @@ -11575,7 +11923,7 @@ snapshots: dependencies: content-type: 2.1.0 - nitropack@2.13.4(@vercel/functions@3.9.5(ws@8.21.3))(oxc-parser@0.143.0)(rolldown@1.2.5)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)): + nitropack@2.13.4(@vercel/functions@3.9.7(ws@8.21.3))(oxc-parser@0.143.0)(rolldown@1.2.5)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.62.5) @@ -11642,7 +11990,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 6.4.0(esbuild@0.28.2)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) unplugin-utils: 0.3.2 - unstorage: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + unstorage: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) untyped: 2.0.0 unwasm: 0.5.3 youch: 4.1.1 @@ -11726,7 +12074,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt-agent-discovery@0.5.1(3adf33016b78b1d9f3d0bf8940d45a28): + nuxt-agent-discovery@0.5.1(93fae588a377ff5ba3fd984718cb8b37): dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) defu: 6.1.7 @@ -11734,9 +12082,9 @@ snapshots: ufo: 1.6.4 yaml: 2.9.0 optionalDependencies: - '@nuxtjs/mcp-toolkit': 0.18.1(@vue/compiler-sfc@3.5.41)(h3@1.15.11)(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(zod@4.4.3) - '@nuxtjs/robots': 6.2.0(3bc4744619a624c5169bc56293f3c985) - '@nuxtjs/sitemap': 8.5.0(3bc4744619a624c5169bc56293f3c985) + '@nuxtjs/mcp-toolkit': 0.18.1(@vue/compiler-sfc@3.5.41)(h3@1.15.11)(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(zod@4.6.1) + '@nuxtjs/robots': 6.2.0(9963a669bf37ac17e292d4f6f824546f) + '@nuxtjs/sitemap': 8.5.0(9963a669bf37ac17e292d4f6f824546f) comark: 0.6.2(beautiful-mermaid@1.1.3)(rangi@2.2.0)(shiki@4.4.3) nuxt-llms: https://pkg.pr.new/nuxt-content/nuxt-llms/nuxt-llms@f6a9730(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) transitivePeerDependencies: @@ -11756,7 +12104,7 @@ snapshots: - rolldown - unplugin - nuxt-og-image@6.7.8(d69daeae2668f4062a5626b724092832): + nuxt-og-image@6.7.8(02a9fd06dc986c7b60d00a5f031b9e09): dependencies: '@clack/prompts': 1.7.0 '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) @@ -11772,8 +12120,8 @@ snapshots: magic-string: 1.2.3 magicast: 0.5.4 mocked-exports: 0.1.1 - nuxt-site-config: 4.2.3(3bc4744619a624c5169bc56293f3c985) - nuxtseo-shared: 5.3.14(fc4f002cf8929f5932e9df9bf8f19792) + nuxt-site-config: 4.2.3(9963a669bf37ac17e292d4f6f824546f) + nuxtseo-shared: 5.3.14(0f8908bb880cd164227784aaff03d6a3) nypm: 0.6.9 object-identity: 0.2.3 ofetch: 1.5.1 @@ -11789,15 +12137,15 @@ snapshots: ufo: 1.6.4 ultrahtml: 1.7.0 unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) - unstorage: 1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) + unstorage: 1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)) optionalDependencies: '@resvg/resvg-js': 2.6.2 - fontless: 0.2.1(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) - nitropack: 2.13.4(@vercel/functions@3.9.5(ws@8.21.3))(oxc-parser@0.143.0)(rolldown@1.2.5)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + fontless: 0.2.1(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + nitropack: 2.13.4(@vercel/functions@3.9.7(ws@8.21.3))(oxc-parser@0.143.0)(rolldown@1.2.5)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) satori: 0.29.1 tailwindcss: 4.3.3 unifont: 0.7.5 - zod: 4.4.3 + zod: 4.6.1 transitivePeerDependencies: - '@farmfe/core' - '@nuxt/schema' @@ -11814,18 +12162,18 @@ snapshots: - vue - webpack - nuxt-schema-org@6.3.1(922ee4371f2ec5825b5f186a85055e54): + nuxt-schema-org@6.3.1(374ede1556afd53e77a90e7d78db3c9d): dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) defu: 6.1.7 - nuxt-site-config: 4.2.3(3bc4744619a624c5169bc56293f3c985) - nuxtseo-shared: 5.3.14(fc4f002cf8929f5932e9df9bf8f19792) + nuxt-site-config: 4.2.3(9963a669bf37ac17e292d4f6f824546f) + nuxtseo-shared: 5.3.14(0f8908bb880cd164227784aaff03d6a3) pkg-types: 2.3.1 ufo: 1.6.4 optionalDependencies: '@unhead/vue': 3.4.0(@oxc-project/types@0.146.0)(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) unhead: 3.4.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) - zod: 4.4.3 + zod: 4.6.1 transitivePeerDependencies: - '@nuxt/schema' - magic-string @@ -11837,7 +12185,7 @@ snapshots: - vite - vue - nuxt-seo-utils@8.4.2(6c2732a7424285fd39f1e74aab3b66f7): + nuxt-seo-utils@8.5.0(0e3d028a172b021df1bc33f9e04f4227): dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) citty: 0.2.2 @@ -11845,9 +12193,9 @@ snapshots: defu: 6.1.7 escape-string-regexp: 5.0.0 exsolve: 1.1.1 - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) - nuxt-site-config: 4.2.3(3bc4744619a624c5169bc56293f3c985) - nuxtseo-shared: 5.3.14(fc4f002cf8929f5932e9df9bf8f19792) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt-site-config: 4.2.3(9963a669bf37ac17e292d4f6f824546f) + nuxtseo-shared: 5.3.14(0f8908bb880cd164227784aaff03d6a3) pathe: 2.0.3 pkg-types: 2.3.1 scule: 1.3.0 @@ -11858,7 +12206,7 @@ snapshots: esbuild: 0.28.2 lightningcss: 1.33.0 rolldown: 1.2.5 - sharp: 0.35.3(@types/node@26.2.0) + sharp: 0.35.4(@types/node@26.2.0) unhead: 3.4.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) transitivePeerDependencies: - '@nuxt/schema' @@ -11885,7 +12233,7 @@ snapshots: - unplugin - vue - nuxt-site-config@4.2.3(3bc4744619a624c5169bc56293f3c985): + nuxt-site-config@4.2.3(9963a669bf37ac17e292d4f6f824546f): dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) @@ -11893,7 +12241,7 @@ snapshots: defu: 6.1.7 h3: 1.15.11 nuxt-site-config-kit: 4.2.3(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vue@3.5.41(typescript@6.0.3)) - nuxtseo-shared: 5.3.14(fc4f002cf8929f5932e9df9bf8f19792) + nuxtseo-shared: 5.3.14(0f8908bb880cd164227784aaff03d6a3) pathe: 2.0.3 pkg-types: 2.3.1 site-config-stack: 4.2.3(vue@3.5.41(typescript@6.0.3)) @@ -11910,16 +12258,27 @@ snapshots: - vite - zod - nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0): + nuxt-workers@0.1.0(magicast@0.5.4): + dependencies: + '@nuxt/kit': 3.21.11(magicast@0.5.4) + magic-string: 0.30.21 + mlly: 1.8.2 + pathe: 2.0.3 + ufo: 1.6.4 + unplugin: 2.3.11 + transitivePeerDependencies: + - magicast + + nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0): dependencies: '@dxup/nuxt': 0.5.10(esbuild@0.28.2)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.2)(magicast@0.5.4)(supports-color@10.2.2) - '@nuxt/devtools': 3.4.2(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(oxc-parser@0.143.0)(rolldown@1.2.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@nuxt/devtools': 3.4.2(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.3)(oxc-parser@0.143.0)(rolldown@1.2.5)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) - '@nuxt/nitro-server': 4.5.2(80fa4b35290e6e68e37c82a4f05aec13) + '@nuxt/nitro-server': 4.5.2(188bc3c2c4363fd52825c16fd4cbef34) '@nuxt/schema': 4.5.2 '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))) - '@nuxt/vite-builder': 4.5.2(5ea4d7f0831f7e722792e954b23a4f93) + '@nuxt/vite-builder': 4.5.2(26f4d21de89f1c50f9572a8528f34eef) '@unhead/vue': 3.4.0(@oxc-project/types@0.146.0)(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) '@vue/shared': 3.5.41 chokidar: 5.0.0 @@ -12051,17 +12410,17 @@ snapshots: - xml2js - yaml - nuxtseo-layer-devtools@5.3.14(9f33b9b9e7e20aae9f2d2c74340473d1): + nuxtseo-layer-devtools@5.3.14(ca9ba60081d8b5129f3ddf17535a8092): dependencies: '@iconify-json/carbon': 1.2.25 '@nuxt/devtools-kit': 4.0.0-alpha.7(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) '@nuxt/kit': 4.5.2(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))) - '@nuxt/ui': 4.11.1(0716ad8e1ed9af83500c5a16dcdaaa58) + '@nuxt/ui': 4.11.0(4df280129450d67e9f51cc0909bcdbf1) '@shikijs/langs': 4.4.3 '@shikijs/themes': 4.4.3 '@vueuse/core': 14.4.0(vue@3.5.41(typescript@6.0.3)) - '@vueuse/nuxt': 14.4.0(fc4d6faa55285c3ac9a966d10279f90e) - nuxtseo-shared: 5.3.14(fc4f002cf8929f5932e9df9bf8f19792) + '@vueuse/nuxt': 14.4.0(400f64eee5a6ea31a3859bc8b5e58b0e) + nuxtseo-shared: 5.3.14(0f8908bb880cd164227784aaff03d6a3) ofetch: 1.5.1 shiki: 4.4.3 tailwindcss: 4.3.3 @@ -12151,7 +12510,7 @@ snapshots: - yup - zod - nuxtseo-shared@5.3.14(fc4f002cf8929f5932e9df9bf8f19792): + nuxtseo-shared@5.3.14(0f8908bb880cd164227784aaff03d6a3): dependencies: '@clack/prompts': 1.7.0 '@nuxt/devtools-kit': 4.0.0-alpha.7(magic-string@1.2.3)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.5)(rollup@4.62.5)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) @@ -12160,7 +12519,7 @@ snapshots: birpc: 4.2.0 consola: 3.4.2 defu: 6.1.7 - nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.5(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.146.0)(@types/node@26.2.0)(@vercel/functions@3.9.7(ws@8.21.3))(@vue/compiler-sfc@3.5.41)(db0@0.3.4)(esbuild@0.28.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.5)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.5)(rollup@4.62.5))(rollup@4.62.5)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.50.0)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) nypm: 0.6.9 ofetch: 1.5.1 pathe: 2.0.3 @@ -12171,8 +12530,8 @@ snapshots: ufo: 1.6.4 vue: 3.5.41(typescript@6.0.3) optionalDependencies: - nuxt-site-config: 4.2.3(3bc4744619a624c5169bc56293f3c985) - zod: 4.4.3 + nuxt-site-config: 4.2.3(9963a669bf37ac17e292d4f6f824546f) + zod: 4.6.1 transitivePeerDependencies: - magic-string - magicast @@ -12635,6 +12994,10 @@ snapshots: punycode@2.3.1: {} + qified@0.10.1: + dependencies: + hookified: 2.2.0 + qs@6.15.3: dependencies: es-define-property: 1.0.1 @@ -12719,6 +13082,22 @@ snapshots: dependencies: jsesc: 3.1.0 + reka-ui@2.10.3(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@floating-ui/dom': 1.8.0 + '@floating-ui/vue': 1.1.11(vue@3.5.41(typescript@6.0.3)) + '@internationalized/date': 3.12.3 + '@internationalized/number': 3.6.7 + '@tanstack/vue-virtual': 3.13.36(vue@3.5.41(typescript@6.0.3)) + '@vueuse/core': 14.4.0(vue@3.5.41(typescript@6.0.3)) + '@vueuse/shared': 14.4.0(vue@3.5.41(typescript@6.0.3)) + aria-hidden: 1.2.6 + defu: 6.1.7 + ohash: 2.0.12 + vue: 3.5.41(typescript@6.0.3) + transitivePeerDependencies: + - '@vue/composition-api' + reka-ui@2.10.4(vue@3.5.41(typescript@6.0.3)): dependencies: '@floating-ui/dom': 1.8.0 @@ -12919,37 +13298,37 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.35.3(@types/node@26.2.0): + sharp@0.35.4(@types/node@26.2.0): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.3 - '@img/sharp-darwin-x64': 0.35.3 - '@img/sharp-freebsd-wasm32': 0.35.3 - '@img/sharp-libvips-darwin-arm64': 1.3.2 - '@img/sharp-libvips-darwin-x64': 1.3.2 - '@img/sharp-libvips-linux-arm': 1.3.2 - '@img/sharp-libvips-linux-arm64': 1.3.2 - '@img/sharp-libvips-linux-ppc64': 1.3.2 - '@img/sharp-libvips-linux-riscv64': 1.3.2 - '@img/sharp-libvips-linux-s390x': 1.3.2 - '@img/sharp-libvips-linux-x64': 1.3.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 - '@img/sharp-linux-arm': 0.35.3 - '@img/sharp-linux-arm64': 0.35.3 - '@img/sharp-linux-ppc64': 0.35.3 - '@img/sharp-linux-riscv64': 0.35.3 - '@img/sharp-linux-s390x': 0.35.3 - '@img/sharp-linux-x64': 0.35.3 - '@img/sharp-linuxmusl-arm64': 0.35.3 - '@img/sharp-linuxmusl-x64': 0.35.3 - '@img/sharp-webcontainers-wasm32': 0.35.3 - '@img/sharp-win32-arm64': 0.35.3 - '@img/sharp-win32-ia32': 0.35.3 - '@img/sharp-win32-x64': 0.35.3 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 '@types/node': 26.2.0 optional: true @@ -13512,7 +13891,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - unstorage@1.17.5(@vercel/functions@3.9.5(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)): + unstorage@1.17.5(@vercel/functions@3.9.7(ws@8.21.3))(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -13523,7 +13902,7 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.4 optionalDependencies: - '@vercel/functions': 3.9.5(ws@8.21.3) + '@vercel/functions': 3.9.7(ws@8.21.3) db0: 0.3.4 ioredis: 5.11.1(supports-color@10.2.2) @@ -13562,6 +13941,14 @@ snapshots: vary@1.1.2: {} + vaul-vue@0.4.1(reka-ui@2.10.3(vue@3.5.41(typescript@6.0.3)))(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@vueuse/core': 10.11.1(vue@3.5.41(typescript@6.0.3)) + reka-ui: 2.10.3(vue@3.5.41(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + transitivePeerDependencies: + - '@vue/composition-api' + vaul-vue@0.4.1(reka-ui@2.10.4(vue@3.5.41(typescript@6.0.3)))(vue@3.5.41(typescript@6.0.3)): dependencies: '@vueuse/core': 10.11.1(vue@3.5.41(typescript@6.0.3)) @@ -13613,7 +14000,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.14.5(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3)): + vite-plugin-checker@0.14.5(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3)): dependencies: '@babel/code-frame': 7.29.7 chokidar: 5.0.0 @@ -13624,7 +14011,7 @@ snapshots: tiny-invariant: 1.3.3 vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) optionalDependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) optionator: 0.9.4 typescript: 6.0.3 vue-tsc: 3.3.11(typescript@6.0.3) @@ -13718,10 +14105,10 @@ snapshots: vue-devtools-stub@0.1.0: {} - vue-eslint-parser@10.4.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + vue-eslint-parser@10.4.1(eslint@10.10.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.0(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.10.0(jiti@2.7.0)(supports-color@10.2.2) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 @@ -13901,12 +14288,12 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-to-json-schema@3.25.2(zod@4.4.3): + zod-to-json-schema@3.25.2(zod@4.6.1): dependencies: - zod: 4.4.3 + zod: 4.6.1 zod@4.1.11: {} - zod@4.4.3: {} + zod@4.6.1: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3d1a108..6ca3246 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,7 +10,8 @@ blockExoticSubdeps: false minimumReleaseAgeExclude: # First-party packages, published and consumed here in the same session. - - '@comark/*' + - '@comark/nuxt' + - '@comark/vue' - comark - comark-content - nuxt-agent-discovery diff --git a/server/api/code-explorer/[...path].get.ts b/server/api/code-explorer/[...path].get.ts index 6edb4bb..1bd84ed 100644 --- a/server/api/code-explorer/[...path].get.ts +++ b/server/api/code-explorer/[...path].get.ts @@ -3,7 +3,7 @@ import { parseMarkdown, type MarkdownDocument } from 'comark' import rangi from 'comark/plugins/rangi' import fs from 'comark-content/sources/fs' import github from 'comark-content/sources/github' -import { geistTheme } from '../../../utils/geist-theme.ts' +import { geistTheme } from '../../../utils/geist.ts' // A read source for one example directory. Dev: working tree. Prod: authenticated GitHub — the repo may be // private, so jsDelivr / unauthenticated raw are out. Mirrors {@link contentSource}'s dev/prod split. diff --git a/server/api/content/[...path].get.ts b/server/api/content/[...path].get.ts index e10db39..c4f39d7 100644 --- a/server/api/content/[...path].get.ts +++ b/server/api/content/[...path].get.ts @@ -1,6 +1,6 @@ /** - * Single data endpoint: `content.handler()` dispatches `get`, `navigation`, `list` and custom handlers - * (e.g. `search-sections`). Cached per-URL — see `routeRules`. + * Single data endpoint: `content.handler()` dispatches `get`, `navigation`, `list`, `manifest` + * and `snapshot`. Must be cached per-URL by layer consumer. */ export default defineEventHandler(async (event) => { const content = await getProdContent() diff --git a/server/api/content/blob/[sha]/[...path].get.ts b/server/api/content/blob/[sha]/[...path].get.ts index a72ef7f..173c7ee 100644 --- a/server/api/content/blob/[sha]/[...path].get.ts +++ b/server/api/content/blob/[sha]/[...path].get.ts @@ -20,7 +20,6 @@ export default defineEventHandler(async (event) => { // Also resolves short SHAs so one commit pins one content instance. const fullSha = await authorizePreviewSha(sha) - const content = await getPreviewContent(fullSha, `/api/content/blob/${sha}`) - - return await content.handler(toWebRequest(event)) + // Head-of-branch requests reuse the shared prod instance; `servePreview()` handles that. + return servePreview(event, fullSha, `/blob/${rawSha}`) }) diff --git a/server/api/content/head.get.ts b/server/api/content/head.get.ts new file mode 100644 index 0000000..493c5bb --- /dev/null +++ b/server/api/content/head.get.ts @@ -0,0 +1,10 @@ +/** + * The commit SHA production content is pinned to, or `null` in dev. + */ +export default defineEventHandler(async () => { + if (import.meta.dev) return { sha: null } + + // Same resolution as the pages (`getProdContent`), so the search artifacts the client hydrates + // from can't come from a different commit than the rendered content — notably under a pin. + return { sha: await resolveProdSha() } +}) diff --git a/server/api/content/pr/[number]/[...path].get.ts b/server/api/content/pr/[number]/[...path].get.ts index 486a679..75de79e 100644 --- a/server/api/content/pr/[number]/[...path].get.ts +++ b/server/api/content/pr/[number]/[...path].get.ts @@ -1,6 +1,6 @@ /** * Per-pull-request data endpoint: `/pr/:number` previews the PR's head commit. Follows new pushes - * (the number → head SHA pointer lives in the short-TTL ref cache) and enforces the preview + * (the number → head SHA pointer lives in the 10-minute ref cache) and enforces the preview * authorization: same-repo PRs always, fork PRs only with the `preview:enabled` label. */ export default defineEventHandler(async (event) => { @@ -17,7 +17,5 @@ export default defineEventHandler(async (event) => { } const sha = await resolvePullPreviewSha(number) - const content = await getPreviewContent(sha, `/api/content/pr/${number}`) - - return await content.handler(toWebRequest(event)) + return servePreview(event, sha, `/pr/${rawNumber}`) }) diff --git a/server/api/content/tree/[branch]/[...path].get.ts b/server/api/content/tree/[branch]/[...path].get.ts index 362414c..0899003 100644 --- a/server/api/content/tree/[branch]/[...path].get.ts +++ b/server/api/content/tree/[branch]/[...path].get.ts @@ -14,7 +14,5 @@ export default defineEventHandler(async (event) => { // `cacheMisses`: the ref comes from the URL, so a miss must not re-cost a GitHub call each time. const sha = await resolveContentSha(branch, useRuntimeConfig(event).docs.contentDir, { cacheMisses: true }) - const content = await getPreviewContent(sha, `/api/content/tree/${encodeURIComponent(branch)}`) - - return await content.handler(toWebRequest(event)) + return servePreview(event, sha, `/tree/${rawBranch}`) }) diff --git a/server/api/revalidate.post.ts b/server/api/revalidate.post.ts index 4a7451d..2c4f9c6 100644 --- a/server/api/revalidate.post.ts +++ b/server/api/revalidate.post.ts @@ -1,30 +1,26 @@ -import type { ContentListFile } from 'comark-content' import { verify } from '@octokit/webhooks-methods' +import { rawUrl } from '#agent-discovery' +import { DEFAULT_CONTENT_NAME } from 'comark-content' import { waitUntil } from '@vercel/functions' /** Each re-render hits this same deployment, so the ceiling is about not stampeding ourselves. */ const REVALIDATE_CONCURRENCY = 8 -/** `Promise.allSettled` over `items`, at most `size` in flight. */ -async function settleInBatches( - items: T[], - size: number, - fn: (item: T) => Promise -): Promise[]> { - const results: PromiseSettledResult[] = [] - for (let i = 0; i < items.length; i += size) { - // Not `.map(fn)` — `map` passes the index, which lands in the callee's optional parameter. - results.push(...(await Promise.allSettled(items.slice(i, i + size).map((item) => fn(item))))) - } - return results -} +/** Why a route was purged — one value per `addPath` call site below. */ +type PurgeReason = 'page' | 'payload' | 'raw' | 'nav' | 'global' + +/** Display/response order. */ +const REASON_ORDER: PurgeReason[] = ['page', 'payload', 'raw', 'nav', 'global'] + +/** `nav` is the only unbounded reason (the whole site can be thousands of pages) — cap what the log prints. */ +const MAX_LOGGED_PATHS_PER_REASON = 5 export default defineEventHandler(async (event) => { const { docs } = useRuntimeConfig(event) const secret = docs.webhookSecret || process.env.WEBHOOK_SECRET const bypassToken = docs.bypassToken || process.env.VERCEL_BYPASS_TOKEN if (!secret || !bypassToken) { - throw createError({ statusCode: 500, statusMessage: 'Webhook not configured' }) + throw createError({ statusCode: 501, statusMessage: 'Revalidation webhook is not configured' }) } const signature = getHeader(event, 'x-hub-signature-256') @@ -41,182 +37,224 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 401, statusMessage: 'Invalid signature' }) } + const requestId = getHeader(event, 'x-vercel-id') ?? getHeader(event, 'x-request-id') ?? 'local' + const deliveryId = getHeader(event, 'x-github-delivery') + const tag = `[revalidate:${requestId}${deliveryId ? `:${deliveryId}` : ''}]` + const timings = createTimings() + + const githubEvent = getHeader(event, 'x-github-event') + if (githubEvent !== 'push') { + console.log(`${tag} skipped: ${githubEvent ?? 'unknown'} event`) + return { ok: true, skipped: 'not-a-push-event', event: githubEvent } + } + const payload = JSON.parse(raw) as GitHubPushPayload const branch = targetBranch() const contentDir = docs.contentDir const expectedRef = `refs/heads/${branch}` - if (payload.ref !== expectedRef) { - console.log(`[content] revalidate push skipped (ref=${payload.ref} !== expected=${expectedRef})`) - return { - ok: true, - skipped: 'non-target-branch', - expected: expectedRef, - received: payload.ref, - } + const repo = githubRepo() + if (payload.repository?.full_name && payload.repository.full_name !== repo) { + console.log(`${tag} skipped: repo=${payload.repository.full_name} !== expected=${repo}`) + return { ok: true, skipped: 'wrong-repo', expected: repo, received: payload.repository.full_name } } - // Classify changed content files. A file added in one commit and modified in - // another counts as added; `.navigation.*` config files always touch navigation. - const added = new Set() - const removed = new Set() - const modified = new Set() - let navConfigTouched = false - for (const commit of payload.commits ?? []) { - for (const f of commit.added ?? []) { - if (isContentMd(f)) added.add(f) - else if (isNavConfig(f)) navConfigTouched = true - } - for (const f of commit.modified ?? []) { - if (isContentMd(f)) modified.add(f) - else if (isNavConfig(f)) navConfigTouched = true - } - for (const f of commit.removed ?? []) { - if (isContentMd(f)) removed.add(f) - else if (isNavConfig(f)) navConfigTouched = true - } + if (payload.ref !== expectedRef) { + console.log(`${tag} skipped: ref=${payload.ref} !== expected=${expectedRef}`) + return { ok: true, skipped: 'non-target-branch', expected: expectedRef, received: payload.ref } } - for (const f of added) modified.delete(f) - const changedFiles = [...added, ...modified, ...removed] - if (changedFiles.length === 0 && !navConfigTouched) { + const changes = changesForPush(contentDir, payload.commits ?? []) + if (!changes.upserted.length && !changes.removed.length && !changes.navTouched) { return { ok: true, skipped: 'no-content-changes' } } - const protocol = getRequestProtocol(event) - const host = getRequestHost(event, { xForwardedHost: true }) - const baseURL = `${protocol}://${host}` - - // `x-vercel-protection-bypass` bypasses the SSO wall when the handler calls itself - const readHeaders: Record = {} - if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) { - readHeaders['x-vercel-protection-bypass'] = process.env.VERCEL_AUTOMATION_BYPASS_SECRET - } - - // `x-prerender-revalidate` purges the ISR cache - const headers: Record = { - ...readHeaders, - 'x-prerender-revalidate': bypassToken, - } + const buildId = useRuntimeConfig(event).app.buildId + const pathsToPurge = new Set() + const byReason = new Map>() + const rawPathFor = (path: string): string => new URL(rawUrl(event, path)).pathname - const headSha = payload.head_commit?.id - if (!headSha) { - throw createError({ statusCode: 400, statusMessage: 'Missing head commit SHA' }) + /** Add a path to the purge set, and track it by reason for the breakdown log. */ + const addPath = (reason: PurgeReason, path: string): void => { + if (pathsToPurge.has(path)) return + pathsToPurge.add(path) + const paths = byReason.get(reason) ?? new Set() + paths.add(path) + byReason.set(reason, paths) } - // Bypass the short ref cache and write the canonical path-filtered revision before the purge fan-out, - // so a freshly-purged page cannot re-render against a stale or payload-order-dependent content SHA. - const contentSha = await resolveContentSha(branch, contentDir, { refresh: true }) - - console.log(`[content] revalidate push headSha=${headSha} contentSha=${contentSha}`) + // Diffed against the live prod instance, already warm + const { headSha, newItems, pagePaths, navChanged } = await timings.time('rebuild', async () => { + const outdated = await getProdContent() + await outdated.init() + const oldItems = { ...(await outdated.manifest()).items } - const requestId = getHeader(event, 'x-vercel-id') ?? getHeader(event, 'x-request-id') ?? 'local' - const tag = `[revalidate:${requestId}]` - - // Planning before we respond costs two `init()` passes against GitHub's ~10s delivery timeout, - // in exchange for diagnostics in the webhook body. Safe because everything here is idempotent, - // so a retried delivery only repeats work. If it gets slow, move this into `waitUntil`. - const beforeSha = payload.before - let oldItems: Record = {} - if (beforeSha && !/^0+$/.test(beforeSha)) { - try { - const oldContent = await createSourceContent(beforeSha) - await oldContent.init() - oldItems = (await oldContent.manifest()).items - } catch (err) { - const message = err instanceof Error ? err.message : err - console.warn(`${tag} no before-manifest (${beforeSha}) — treating as full revalidate:`, message) - } - } + // Refresh the content SHA + const headSha = await resolveContentSha(branch, contentDir, { refresh: true }) - // The head snapshot has the same content directory as `contentSha`; populate the namespace that - // production instances will read even when later commits in this push only changed code. - const headContent = await createSourceContent(headSha, { cache: { driver: cacheDriver(contentSha) } }) - await headContent.init() - const newItems = (await headContent.manifest()).items + // Throwaway instance: the diff needs the index only. + // It lands in the commit's cache namespace to be reused by the prod warm below. + const fresh = contentAt(headSha) + await fresh.init() + const newItems = (await fresh.manifest()).items + await fresh.dispose() - const oldPaths = new Set(Object.keys(oldItems)) - const newPaths = Object.keys(newItems) - const addedPaths = newPaths.filter((p) => !oldPaths.has(p)) - const removedPaths = [...oldPaths].filter((p) => !(p in newItems)) + return { headSha, newItems, ...diffContent(changes, oldItems, newItems) } + }) - const metaChangedPaths: string[] = [] - for (const p of newPaths) { - if (oldPaths.has(p) && hashManifestItem(oldItems[p]) !== hashManifestItem(newItems[p])) metaChangedPaths.push(p) - } - const navChanged = navConfigTouched || addedPaths.length > 0 || removedPaths.length > 0 || metaChangedPaths.length > 0 - - // Payload routes are keyed by the build-id query on some deployments, so purge the exact - // URL the browser loads (`…/_payload.json?`). - const buildId = useRuntimeConfig(event).app.buildId - - // Any content change invalidates the llms indexes, the markdown sitemap, the feed, and the body-derived - // search index. - const paths = new Set(['/llms.txt', '/llms-full.txt', '/sitemap.md', '/rss.xml', '/api/content/search-sections']) - for (const f of changedFiles) { - const pageUrl = pageUrlForPath(f) - if (pageUrl) { - paths.add(payloadUrlForRoute(pageUrl, buildId)) - paths.add(pageUrl) - } - const rawUrl = rawUrlForPath(f) - if (rawUrl) paths.add(rawUrl) + for (const path of pagePaths) { + addPath('page', path) + addPath('payload', payloadUrlForPage(path, buildId)) + addPath('raw', rawPathFor(path)) } // Navigation renders on every page, so a change to it re-renders all of them. if (navChanged) { for (const item of Object.values(newItems)) { - if (item.meta.kind === 'document') { - paths.add(item.path) - paths.add(payloadUrlForRoute(item.path, buildId)) - } + if (item.meta.kind !== 'document') continue + addPath('nav', item.path) + addPath('nav', payloadUrlForPage(item.path, buildId)) + addPath('nav', rawPathFor(item.path)) } } + // Per-commit search artifacts (ISR, immutable). + const artifactBase = `/api/content/blob/${headSha}` + const pathsToWarm = [`${artifactBase}/manifest.json`, `${artifactBase}/snapshot/${DEFAULT_CONTENT_NAME}.json`] + + // Any content change invalidates the global indexes: each is rebuilt from the whole tree. + for (const path of ['/llms.txt', '/llms-full.txt', '/rss.xml', '/sitemap.xml', '/sitemap.md']) { + addPath('global', path) + } + console.log( `${tag} navChanged=${navChanged} ` + - `(added=${addedPaths.length}, removed=${removedPaths.length}, meta=${metaChangedPaths.length}, navConfig=${navConfigTouched}) | ` + - `files: +${added.size} ~${modified.size} -${removed.size} | ${paths.size} route(s)` + `(upserted=${changes.upserted.length}, removed=${changes.removed.length}, navConfig=${changes.navTouched}) | ` + + `${pathsToPurge.size} to purge, ${pathsToWarm.length} to warm | ${timings.format()}` ) - if (metaChangedPaths.length) console.log(`${tag} meta changed: ${metaChangedPaths.join(', ')}`) - if (addedPaths.length) console.log(`${tag} added: ${addedPaths.join(', ')}`) - if (removedPaths.length) console.log(`${tag} removed: ${removedPaths.join(', ')}`) + + logBreakdown(tag, byReason) + for (const path of pathsToWarm) console.log(`${tag} warm\t${path}`) + + // Dev has no ISR cache to purge + if (import.meta.dev) { + return { + ok: true, + requestId, + deliveryId, + navChanged, + routes: routesBreakdown(byReason), + warm: pathsToWarm.length, + dev: true, + } + } + + const protocol = getRequestProtocol(event) + const host = getRequestHost(event, { xForwardedHost: true }) + const baseURL = `${protocol}://${host}` + + // Lets the deployment call itself while Vercel Authentication is on (preview deploys). + const selfCall: Record = process.env.VERCEL_AUTOMATION_BYPASS_SECRET + ? { 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET } + : {} + + // `x-prerender-revalidate` regenerates the ISR entry for the URL being fetched. + const purgeHeaders = { ...selfCall, 'x-prerender-revalidate': bypassToken } // Vercel's native waitUntil, not Nitro's `event.waitUntil` — that one can orphan async work here. waitUntil( (async () => { - const revalidate = (path: string, extra: Record = {}) => - $fetch(path, { baseURL, method: 'GET', headers: { ...headers, ...extra } }).catch((err) => { - console.error(`${tag} ✗ ${path}`, err?.statusCode ?? err?.message ?? err) - throw err - }) - - // Warm the per-SHA body cache so cold instances skip re-parsing from GitHub. - // `metaOnly` became `partial` in comark-content 0.2.0 with no alias and consumers straddle both, - // so send both keys — each version ignores the other's. Not inlined: as a literal, - // excess-property checking rejects whichever key the installed types don't declare. - const full = { partial: false, metaOnly: false } - await headContent.init(full).catch((err) => { - console.error(`${tag} cache warm failed`, err?.message ?? err) - }) - - await useStorage('cache:nuxt:payload').clear() - - // Bounded: a nav change queues two URLs per page, and every one re-enters this function. - const results = await settleInBatches([...paths], REVALIDATE_CONCURRENCY, revalidate) - const ok = results.filter((r) => r.status === 'fulfilled').length - console.log(`${tag} complete: ${ok}/${results.length} succeeded`) + const absent: string[] = [] + + // The warm runs first: + // - ISR cache manifest and snapshot for the new SHA + // - Cache parsed items for the pages to purge and re-render + const warmResults = await timings.time('warm', () => + settleInBatches(pathsToWarm, REVALIDATE_CONCURRENCY, (path) => + $fetch(path, { baseURL, method: 'GET', headers: selfCall }).catch((error) => { + console.error(`${tag} ✗ ${path}`, error?.statusCode ?? error?.message ?? error) + throw error + }) + ) + ) + + const purgeResults = await timings.time('purge', () => + settleInBatches([...pathsToPurge], REVALIDATE_CONCURRENCY, (path) => + $fetch(path, { baseURL, method: 'GET', headers: purgeHeaders }).catch((error) => { + // Content with no page of its own (e.g. a partial) has nothing cached to purge. + if (error?.statusCode === 404) { + absent.push(path) + return + } + console.error(`${tag} ✗ ${path}`, error?.statusCode ?? error?.message ?? error) + throw error + }) + ) + ) + + const warmed = warmResults.filter((r) => r.status === 'fulfilled').length + const failed = [...warmResults, ...purgeResults].filter((r) => r.status === 'rejected').length + const purged = purgeResults.filter((r) => r.status === 'fulfilled').length - absent.length + console.log( + `${tag} complete: ${warmed} warmed, ${purged} purged, ${absent.length} absent, ` + + `${failed} failed | ${timings.format()} | total=${timings.since()}ms` + ) + logAbsent(tag, absent) })() ) return { ok: true, requestId, + deliveryId, navChanged, - manifest: { - added: addedPaths, - removed: removedPaths, - metaChanged: metaChangedPaths, - }, + manifest: { upserted: changes.upserted, removed: changes.removed }, + routes: routesBreakdown(byReason), } }) + +/** One log line per purged path, grouped by reason. */ +function logBreakdown(tag: string, byReason: Map>): void { + for (const reason of REASON_ORDER) { + const paths = byReason.get(reason) + if (!paths?.size) continue + + const sorted = [...paths].sort() + for (const path of sorted.slice(0, MAX_LOGGED_PATHS_PER_REASON)) { + console.log(`${tag} ${reason}\t${path}`) + } + if (sorted.length > MAX_LOGGED_PATHS_PER_REASON) { + console.log(`${tag} ${reason}\t... (${sorted.length} total)`) + } + } +} + +/** One log line per absent path — expected to be empty. */ +function logAbsent(tag: string, absent: string[]): void { + for (const path of [...absent].sort()) { + console.log(`${tag} absent\t${path}`) + } +} + +/** Route counts by reason. */ +function routesBreakdown(byReason: Map>): { total: number } & Partial> { + const counts: Partial> = {} + for (const [reason, paths] of byReason) counts[reason] = paths.size + + const total = Object.values(counts).reduce((sum, count) => sum + (count ?? 0), 0) + return { total, ...counts } +} + +/** `Promise.allSettled` over `items`, at most `size` in flight. */ +async function settleInBatches( + items: T[], + size: number, + fn: (item: T) => Promise +): Promise[]> { + const results: PromiseSettledResult[] = [] + for (let i = 0; i < items.length; i += size) { + // Not `.map(fn)` — `map` passes the index, which lands in the callee's optional parameter. + results.push(...(await Promise.allSettled(items.slice(i, i + size).map((item) => fn(item))))) + } + return results +} diff --git a/server/utils/cache.ts b/server/utils/cache.ts index 9c686e1..72f9dc6 100644 --- a/server/utils/cache.ts +++ b/server/utils/cache.ts @@ -5,52 +5,49 @@ import vercelRuntimeCache from 'unstorage/drivers/vercel-runtime-cache' /** SHA-pinned content is immutable, so it can be cached for a long time. */ const TTL = 60 * 60 * 24 -/** Content refs move with their branches, so the pointer cache uses a short TTL. */ -const REF_TTL = 60 - /** Whether the Vercel Runtime Cache is available (i.e. running on Vercel). */ function cacheAvailable(): boolean { return !import.meta.dev && Boolean(process.env.VERCEL) } +/** Every namespace below degrades to per-process memory off Vercel if not available. */ +function runtimeCacheDriver(base: string, ttl?: number): Driver { + if (!cacheAvailable()) return memoryDriver() + return vercelRuntimeCache({ base, ttl }) +} + /** * Bump when content parser/plugin configuration, relevant parser dependencies, or cached derived * data changes. Keeping this explicit lets unrelated deployments reuse immutable content artifacts. */ export const CONTENT_PARSER_VERSION = 'v3' -/** Per-parser-version, per-content-SHA driver backing comark's manifest and parsed bodies. */ -export function cacheDriver(sha: string): Driver { - if (!cacheAvailable()) return memoryDriver() - return vercelRuntimeCache({ - base: `content:${CONTENT_PARSER_VERSION}:${sha}`, - ttl: TTL, - }) +/** + * Comark cache: index, parsed bodies and artifacts of every commit. + * Sharing content across all perser versions BUT keys are per-sha. + */ +export function contentCacheDriver(): Driver { + return runtimeCacheDriver(`content:${CONTENT_PARSER_VERSION}`, TTL) } /** - * Ad-hoc per-SHA storage for non-content data (commit history, RSS dates). Shares comark's - * `cacheDriver(sha)` namespace rather than a separate unconfigured mount; `gh:...` keys can't - * collide with comark's `:`. + * Shared driver backing branch pointers and preview authorization decisions (`github.ts`). The + * caller supplies a bounded default TTL, and individual preview entries can use a shorter TTL. + * + * TODO: Vercel Runtime Cache is **regional**, not global (https://vercel.com/docs/caching/runtime-cache): + * It assumes Functions run in a single region. + * Multi-region would confine the webhook's forced refresh to its region (others self-heal on TTL) + * We should reach for a globally replicated store (e.g. Edge Config). */ -export function shaCacheStorage(sha: string): Storage { - return createStorage({ driver: cacheDriver(sha) }) +export function refCacheDriver(ttl: number): Driver { + // v2 prevents entries written before production refs gained a fallback TTL from surviving the migration. + return runtimeCacheDriver('content:refs:v2', ttl) } /** - * Shared driver backing the branch + content directory → content commit pointer - * (`resolveContentSha` in `github.ts`), in its own namespace so every instance reads one pointer - * instead of keeping its own timer. - * - * Vercel Runtime Cache is **regional**, not global (https://vercel.com/docs/caching/runtime-cache): - * this assumes Functions run in a single region (no `regions` in `vercel.json`/`nuxt.config.ts`). - * Multi-region would confine the webhook's forced refresh to its region — others self-heal on TTL, - * so reach for a globally replicated store (e.g. Edge Config) only if that day comes. + * Per-commit data Comark knows nothing about (commit history, RSS dates). + * Namespace is per-sha and keys start with `gh:`. */ -export function refCacheDriver(): Driver { - if (!cacheAvailable()) return memoryDriver() - return vercelRuntimeCache({ - base: 'content:refs', - ttl: REF_TTL, - }) +export function shaCacheStorage(sha: string): Storage { + return createStorage({ driver: runtimeCacheDriver(`content:${CONTENT_PARSER_VERSION}:${sha}`, TTL) }) } diff --git a/server/utils/content.ts b/server/utils/content.ts index 862de45..8c7455e 100644 --- a/server/utils/content.ts +++ b/server/utils/content.ts @@ -1,103 +1,64 @@ -import { defineContentPlugin, type CacheOptions, comarkContent } from 'comark-content'; +import { type ContentSource, DEFAULT_CONTENT_NAME } from 'comark-content' import fs from 'comark-content/sources/fs' import github from 'comark-content/sources/github' -import rangi from 'comark/plugins/rangi' -import security from 'comark/plugins/security' -import emoji from 'comark/plugins/emoji' -import toc from 'comark/plugins/toc' -import mermaid from 'comark/plugins/mermaid' -import yaml from 'comark-content/plugins/yaml' -import tracingOtel from 'comark-content/plugins/tracing/otel' -import { contentTracer } from './tracer.ts' -import { geistTheme } from '../../utils/geist-theme.ts' +import { withSnapshot } from 'comark-content/sources/snapshot' +import { createRuntimeContentInstance } from '../../utils/content.ts' /** - * The instance this layer builds, derived from the factory rather than written - * out. - * - * `ComarkContent` is the *unnarrowed* shape: its instance-name parameter drives - * the conditional types behind `get()` and `list()`, so a concrete instance is - * not assignable to it. Deriving instead of annotating keeps the narrowing that - * `comark-content prepare` generates — `get('/known/path')` stays typed all the - * way through the layer. + * The instance serving requests in this layer. */ -export type DocsContent = Awaited> - -// Rebuilt only when the head advances (see `getProdContent`). Holds the *promise*, not the instance: the -// assignment lands after the await, so two requests on a cold instance would each build a CMS. -let content: Promise | undefined - -// Bump CONTENT_PARSER_VERSION in `cache.ts` when these plugins or their options change cached output. -const comarkPlugins = [ - mermaid({ theme: 'zinc-light', themeDark: 'zinc-dark' }), - rangi({ theme: geistTheme }), - toc({ depth: 3 }), - emoji(), - security({ - blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style'], - allowDataImages: false, - }), -] - -// Bound to THIS instance so a preview content instance serves its own version's sections, not production's. -const searchSectionsPlugin = defineContentPlugin(() => ({ - name: 'search-sections', - setup(ctx) { - ctx.addServeHandler('search-sections', async () => Response.json(await buildSearchSections(ctx as unknown as DocsContent))) - }, -}))() +export type DocsContent = ReturnType /** - * Create a new content instance reading content at `ref` (a commit SHA or branch). `remote` forces the - * GitHub source, `cache` overrides comark's (in-memory by default), `watch` is dev file watching. + * Holds the source, the plugins and the cache driver. + * Once per function invocation. + * Base for all instances "cloned" with `withRef(sha)` in `contentAt()`. */ -export async function createSourceContent( - ref: string, - opts: { remote?: boolean; cache?: CacheOptions; basePath?: string; watch?: boolean } = {} -) { - // A no-op unless the consumer shadows it from their own `server/utils/`. Re-typed as the layer's own - // options: a consumer's hook is declared against the wide `ContentOptions`, and letting that widen the - // argument would erase the source and plugin types `comarkContent` infers from the literal. - const tracer = contentTracer() - const instance = comarkContent({ - markdown: { - plugins: comarkPlugins, - }, - source: contentSource(ref, { remote: opts.remote }), - plugins: [ - yaml(), // enable .navigation.yml to be detected - searchSectionsPlugin, - tracer && tracingOtel({ tracer }), - ], - cache: opts.cache, - basePath: opts.basePath, - }) +let base: DocsContent | undefined - // Only the default instance watches: others read a fixed ref that can't change, and retaining - // `watch()`'s stop function to release the watcher would leak once the preview entry is evicted. - if (import.meta.dev && opts.watch) { - await instance.watch() - instance.hooks.hook('watch:file:update', (_source:string, key: string) => { - invalidateSearchSections(instance) - console.log(`${key} updated`) - }) - instance.hooks.hook('watch:file:remove', () => invalidateSearchSections(instance)) - } +function getBaseContent(): DocsContent { + base ??= createBaseContent() + return base +} - return instance +function createBaseContent() { + return createRuntimeContentInstance({ + source: contentSource(), + cache: { driver: contentCacheDriver() }, + }) +} + +/** + * Base instance cloned and pinned to a SHA. + * Nothing is read until the first call. + * `dispose()` it when you replace it. +*/ +export function contentAt(sha: string): DocsContent { + return getBaseContent().withRef(sha) } -// The content commit this instance is pinned to. Pinning GitHub reads to an immutable SHA rather -// than the branch name bypasses the stale `raw.githubusercontent.com/` CDN. -let headRef: string | undefined +// The content commit currently served, once `getProdContent()` has resolved one. +// Pin GitHub reads to an immutable SHA: +// bypasses the stale `raw.githubusercontent.com/` CDN. +let headSha: string | undefined +/** + * The pinned head commit, or nothing while none is resolved (dev, off-Vercel, pre-first-resolve). + */ +export function getHeadSha(): string | undefined { + return headSha +} + +/** + * The pinned head SHA, falling back to the branch while none is resolved. + * The fallback only ever applies off-Vercel (self-hosted, `nuxt preview`, `vercel dev`). + */ export function getHeadRef(): string { - headRef ??= targetBranch() - return headRef + return headSha ?? targetBranch() } /** - * The SHA production should currently serve: + * The SHA prod instance currently serves: * - global config pin if one is set (production only) * - latest commit touching the content directory via `resolveContentSha()` */ @@ -110,88 +71,125 @@ export async function resolveProdSha(): Promise { return resolveContentSha(targetBranch(), contentDir) } +// Rebuild the promise when the head advances. +// Holds the promise to ensure two requests on a cold process don't each build one. +let prod: Promise | undefined + /** - * Shared content instance for the lifetime of this server instance, pinned to `headRef`. In production every - * call resolves the current head via `resolveProdSha()` — a shared, short-TTL cache, not a per-instance - * timer — and rebuilds when that advances. Previews stay pinned. + * Shared instance for the lifetime of the process, pinned to `headSha`. + * Always resolves the head via `resolveProdSha()`. + * Swaps to a new pinned instance when the head advances. */ export async function getProdContent(): Promise { if (['production', 'preview'].includes(process.env.VERCEL_ENV || '')) { const sha = await resolveProdSha() - if (sha !== getHeadRef()) { - console.log(`[content] head ${getHeadRef()} -> ${sha}`) - headRef = sha - content = undefined // the old instance baked its source at the old commit + if (sha !== headSha) { + if (headSha) { + console.log(`[comark-docs] New head: ${headSha} -> ${sha}`) + void prod?.then((instance) => instance.dispose()).catch(() => {}) + prod = undefined + } + headSha = sha } } - if (!content) { - content = createSourceContent(getHeadRef(), { - watch: true, - cache: { - driver: cacheDriver(getHeadRef()), - }, - }).catch((error) => { - // Don't memoize a failed build — the next request should retry. - content = undefined + if (!prod) { + prod = (async () => { + const instance = import.meta.dev ? await watchedDevContent() : contentAt(getHeadRef()) + const startedAt = performance.now() + await instance.init() + recordDuration('content.init.ms', startedAt) + return instance + })().catch((error) => { + prod = undefined throw error }) } - return content + return prod } -function contentSource(ref: string, opts: { remote?: boolean } = {}) { +/** The unpinned base in development — it reads the working tree and follows file changes. */ +async function watchedDevContent(): Promise { + const instance = getBaseContent() + await instance.watch() + instance.hooks.hook('watch:file:update', (_source: string, key: string) => { + invalidateSearchSections(instance) + console.log(`[comark-docs] ${key} updated`) + }) + instance.hooks.hook('watch:file:remove', () => invalidateSearchSections(instance)) + return instance +} + +/** + * The source every instance derives from. + * `withRef(sha)` pins it to a commit. + * + * Production: + * - GitHub reads the tree and files at `sha` + * - the build snapshot supplies every body whose source hash is unchanged + * + * Development: + * - unpinned reads the working tree, which `watch()` follows + * - pinned reads the repo at that commit + * - no snapshot + */ +function contentSource(): ContentSource { const { docs } = useRuntimeConfig() if (import.meta.dev) { - if (opts.remote) return gitLocalSource(ref, docs.contentDir) - - return fs(docs.contentPath) + return { + ...fs(docs.contentPath), + withRef: (ref) => gitLocalSource(ref, docs.contentDir), + } } - return github({ + const source = github({ repo: githubRepo(), - branch: ref, + branch: targetBranch(), path: docs.contentDir, token: githubToken(), - // `ref` is an immutable commit SHA => we can cache hard. + // Reads happen through `withRef()`, an immutable commit => cache hard. ttl: 60 * 60 * 24, }) + + // Snaphot build during build time by `modules/snapshot/` is used. + // Snpahost is pinned to the latest commit at the time of the build. + // First head moves, only the bodies whose source hash matches are reused. + return withSnapshot(source, () => readSnapshot(), () => readManifest()) } -/** Per-instance registry of preview CMS instances, keyed by `::`. */ -const contentPreviewInstances = new Map>() - -// Bound required: each entry is a content instance with its own manifest and parsed bodies, and public -// `/tree/:branch` / `/blob/:sha` let a crawler mint one per SHA. Evicted refs just rebuild, their -// bodies surviving in the per-SHA Runtime Cache. -const MAX_PREVIEW_INSTANCES = 8 - -export function getPreviewContent(sha: string, basePath: string): Promise { - const key = `${basePath}::${sha}` - const existing = contentPreviewInstances.get(key) - if (existing) { - // `Map` preserves insertion order, which is the whole LRU: re-insert so the MRU key is last. - contentPreviewInstances.delete(key) - contentPreviewInstances.set(key, existing) - return existing +/** + * Read the build-time snapshot, or nothing when this deployment did not ship one. + */ +async function readSnapshot(): Promise { + const span = contentTracer()?.startSpan('snapshot:read') + const startedAt = performance.now() + try { + // Untyped read: unstorage runs every value through `destr`, so this arrives already parsed. + const data = await useStorage('assets:comark-content').get(`${DEFAULT_CONTENT_NAME}/snapshot.json`) + const hit = data != null + span?.setAttribute('comark.snapshot.hit', hit) + recordDuration('content.snapshot.read.ms', startedAt, { hit: String(hit) }) + return data + } finally { + span?.end() } +} - const instance = createSourceContent(sha, { - remote: true, - basePath, - cache: { driver: cacheDriver(sha) }, - }).catch((error) => { - contentPreviewInstances.delete(key) - throw error - }) - contentPreviewInstances.set(key, instance) - - while (contentPreviewInstances.size > MAX_PREVIEW_INSTANCES) { - const oldest = contentPreviewInstances.keys().next() - if (oldest.done) break - contentPreviewInstances.delete(oldest.value) +/** + * Read the build-time snapshot, or nothing when this deployment did not ship one. + */ +async function readManifest(): Promise { + const span = contentTracer()?.startSpan('manifest:read') + const startedAt = performance.now() + try { + // Untyped read: unstorage runs every value through `destr`, so this arrives already parsed. + const data = await useStorage('assets:comark-content').get(`${DEFAULT_CONTENT_NAME}/manifest.json`) + const hit = data != null + span?.setAttribute('comark.manifest.hit', hit) + recordDuration('content.manifest.read.ms', startedAt, { hit: String(hit) }) + return data + } finally { + span?.end() } - - return instance } diff --git a/server/utils/github.ts b/server/utils/github.ts index eb995a5..c47bdbc 100644 --- a/server/utils/github.ts +++ b/server/utils/github.ts @@ -1,5 +1,6 @@ import { createHash, timingSafeEqual } from 'node:crypto' import { createStorage } from 'unstorage' +import { fetchLastContentCommit } from '../../utils/github' export interface GitHubCommit { added?: string[] @@ -13,6 +14,7 @@ export interface GitHubPushPayload { before?: string commits?: GitHubCommit[] head_commit?: GitHubCommit & { id?: string } + repository?: { full_name?: string } } /** Constant-time string comparison. */ @@ -44,13 +46,25 @@ export function targetBranch(): string { return process.env.VERCEL_GIT_COMMIT_REF || useRuntimeConfig().docs.github.branch || 'main' } -// Branch + content directory → content commit SHA pointer, shared across every instance so only one -// pays for the GitHub API call per TTL window. See `refCacheDriver()` for the single-region assumption. -const refStorage = createStorage({ driver: refCacheDriver() }) +/** The webhook refreshes production immediately; this bounds recovery when delivery fails. */ +const PRODUCTION_REF_TTL = 60 * 60 + +/** Moving previews and negative decisions refresh or recover more frequently. */ +const PREVIEW_REF_TTL = 600 + +// Branch + content directory → content commit SHA pointer, shared across every instance. The +// production branch is refreshed by its push webhook; TTL remains a bounded fallback. +const refStorage = createStorage({ driver: refCacheDriver(PRODUCTION_REF_TTL) }) const normalizeContentDir = (contentDir: string) => contentDir.replace(/^\/+|\/+$/g, '') const refKey = (branch: string, contentDir: string) => `branch:${encodeURIComponent(branch)}:path:${encodeURIComponent(normalizeContentDir(contentDir))}` +/** Production has a longer fallback because its webhook owns the normal refresh path. */ +function refTtl(branch: string): number { + if (process.env.VERCEL_ENV === 'production' && branch === targetBranch()) return PRODUCTION_REF_TTL + return PREVIEW_REF_TTL +} + /** Sentinel for "this ref doesn't resolve" — see the negative caching in `resolveContentSha`. */ const UNRESOLVED = '\0unresolved' @@ -79,38 +93,33 @@ export async function resolveContentSha( if (cached) return cached } - const token = githubToken() - let commits: Array<{ sha: string }> + // Shared with the build-time snapshot, which walks the built commit instead of a branch — see + // `fetchLastContentCommit()`. One query, so the two cannot drift apart. + let sha: string | undefined try { - commits = await $fetch>(`https://api.github.com/repos/${githubRepo()}/commits`, { - headers: { - Accept: 'application/vnd.github+json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - query: { - sha: branch, - path: normalizeContentDir(contentDir), - per_page: 1, - }, + sha = await fetchLastContentCommit({ + repo: githubRepo(), + path: contentDir, + ref: branch, + token: githubToken(), }) } catch (error: unknown) { // Only a definitive 404 is cacheable; a 5xx, rate-limit 403 or network blip stays retryable. const failure = error as { statusCode?: number; response?: { status?: number } } const status = failure.statusCode ?? failure.response?.status if (status === 404) { - if (opts.cacheMisses) await refStorage.setItem(key, UNRESOLVED) + if (opts.cacheMisses) await refStorage.setItem(key, UNRESOLVED, { ttl: PREVIEW_REF_TTL }) throw createError({ statusCode: 404, statusMessage: `Ref not found: ${branch}` }) } throw error } - const sha = commits[0]?.sha if (!sha) { - if (opts.cacheMisses) await refStorage.setItem(key, UNRESOLVED) + if (opts.cacheMisses) await refStorage.setItem(key, UNRESOLVED, { ttl: PREVIEW_REF_TTL }) throw createError({ statusCode: 404, statusMessage: `Content not found at ref: ${branch}` }) } - await refStorage.setItem(key, sha) + await refStorage.setItem(key, sha, { ttl: refTtl(branch) }) return sha } @@ -158,8 +167,8 @@ function pullAllowsPreview(pull: GitHubPullSummary): boolean { * 1. an associated PR allows it (same-repo PR, or a fork PR carrying `preview:enabled`), or * 2. the commit is in the production branch's history (version-history links). * - * Decisions live in the short-TTL ref cache — positive ones too, so removing the label revokes - * access within a TTL. Skipped in dev, where refs resolve against the local checkout instead. + * Decisions live in the preview ref cache — positive ones too, so removing the label revokes + * access within 10 minutes. Skipped in dev, where refs resolve against the local checkout instead. */ export async function authorizePreviewSha(sha: string): Promise { if (import.meta.dev) return sha @@ -172,7 +181,7 @@ export async function authorizePreviewSha(sha: string): Promise { if (cached) return cached const deny = async (): Promise => { - await refStorage.setItem(key, UNRESOLVED) + await refStorage.setItem(key, UNRESOLVED, { ttl: PREVIEW_REF_TTL }) throw createError({ statusCode: 404, statusMessage: `No preview available for commit: ${sha}` }) } @@ -211,7 +220,7 @@ export async function authorizePreviewSha(sha: string): Promise { if (!allowed) return deny() - await refStorage.setItem(key, fullSha) + await refStorage.setItem(key, fullSha, { ttl: PREVIEW_REF_TTL }) return fullSha } @@ -219,8 +228,8 @@ export async function authorizePreviewSha(sha: string): Promise { * Authorize a `/pr/:number` preview and resolve it to the PR's head commit SHA. * * Same rule as `authorizePreviewSha`: same-repo PRs are always previewable, fork PRs only with the - * `preview:enabled` label. Cached in the short-TTL ref cache so the preview follows new pushes and - * label removal revokes it within a TTL. + * `preview:enabled` label. Cached for 10 minutes so the preview follows new pushes and label removal + * revokes it within the same bound. */ export async function resolvePullPreviewSha(number: number): Promise { const key = `preview:pr:${number}` @@ -231,7 +240,7 @@ export async function resolvePullPreviewSha(number: number): Promise { if (cached) return cached const deny = async (): Promise => { - await refStorage.setItem(key, UNRESOLVED) + await refStorage.setItem(key, UNRESOLVED, { ttl: PREVIEW_REF_TTL }) throw createError({ statusCode: 404, statusMessage: `No preview available for PR #${number}` }) } @@ -248,7 +257,7 @@ export async function resolvePullPreviewSha(number: number): Promise { const sha = pull.head?.sha if (!sha || !pullAllowsPreview(pull)) return deny() - await refStorage.setItem(key, sha) + await refStorage.setItem(key, sha, { ttl: PREVIEW_REF_TTL }) return sha } diff --git a/server/utils/local.ts b/server/utils/local.ts index 8d7ae1b..5e92c9b 100644 --- a/server/utils/local.ts +++ b/server/utils/local.ts @@ -4,7 +4,7 @@ import type { Source } from 'comark-content' import type { PageCommit } from './github' const exec = promisify(execFile) -/** Root of the git repository holding the content (resolved at build time by modules/config.ts). */ +/** Root of the git repository holding the content (resolved at build time by modules/config/). */ function repoRoot(): string { return useRuntimeConfig().docs.repoRoot } diff --git a/server/utils/metrics.ts b/server/utils/metrics.ts new file mode 100644 index 0000000..ba656d1 --- /dev/null +++ b/server/utils/metrics.ts @@ -0,0 +1,13 @@ +import { metric } from '@vercel/functions' + +/** + * Report an elapsed time to Vercel Observability, and hand it back for logging. + * + * `metric()` talks to the runtime over an IPC global that only exists on Vercel, so this is a + * no-op locally rather than an error. + */ +export function recordDuration(name: string, startedAt: number, tags?: Record): number { + const ms = Math.round(performance.now() - startedAt) + metric(name, ms, tags) + return ms +} diff --git a/server/utils/paths.ts b/server/utils/paths.ts index ae8f710..4988fa2 100644 --- a/server/utils/paths.ts +++ b/server/utils/paths.ts @@ -1,55 +1,4 @@ -/** Repo-relative content prefix (e.g. `docs/content/`), derived at build time by modules/config.ts. */ +/** Repo-relative content prefix (e.g. `docs/content/`), derived at build time by modules/config/. */ export function contentPrefix(): string { return `${useRuntimeConfig().docs.contentDir.replace(/\/$/, '')}/` } - -/** Whether a GitHub repo path is a content markdown file. */ -export function isContentMd(path: string): boolean { - return path.startsWith(contentPrefix()) && path.toLowerCase().endsWith('.md') -} - -/** Whether a GitHub repo path is a navigation config file (`.navigation.yml` / `.json`). */ -export function isNavConfig(path: string): boolean { - return path.startsWith(contentPrefix()) && /\.navigation\.(?:ya?ml|json)$/i.test(path) -} - -/** - * Parse a content repo path into route segments (`1.getting-started/2.intro.md` → - * `['getting-started', 'intro']`). `isIndex` covers both `index.md` and `index/index.md`. - */ -export function slugFromPath(path: string): { isIndex: boolean; segments: string[] } | null { - const prefix = contentPrefix() - if (!path.startsWith(prefix) || !path.toLowerCase().endsWith('.md')) return null - - const relative = path.slice(prefix.length, -3) - const segments = relative.split('/').map((s) => s.replace(/^\d+\./, '')) - const last = segments[segments.length - 1] - const isIndex = last === 'index' - if (isIndex) segments.pop() - return { isIndex, segments } -} - -/** Frontend page route (e.g. `1.getting-started/2.intro.md` → `/getting-started/intro`, root → `/`). */ -export function pageUrlForPath(path: string): string | null { - const result = slugFromPath(path) - if (!result) return null - const { isIndex, segments } = result - if (isIndex && segments.length === 0) return '/' - return `/${segments.join('/')}` -} - -/** Raw markdown route — the only per-file route that stays cached, as `/api/pages` is served live. */ -export function rawUrlForPath(path: string): string | null { - const result = slugFromPath(path) - if (!result) return null - - const { isIndex, segments } = result - if (isIndex && segments.length === 0) return '/raw/index.md' - return `/raw/${segments.join('/')}.md` -} - -/** Nuxt payload route for a frontend page route */ -export function payloadUrlForRoute(route: string, buildId?: string): string { - const path = `${route === '/' ? '' : route}/_payload.json` - return buildId ? `${path}?${buildId}` : path -} diff --git a/server/utils/preview.ts b/server/utils/preview.ts new file mode 100644 index 0000000..97e9cf9 --- /dev/null +++ b/server/utils/preview.ts @@ -0,0 +1,45 @@ +import type { DocsContent } from './content' + +// Preview instances for `/blob/:sha`, `/tree/:branch` and `/pr/:number`. +const previews = new Map() + +const MAX_PREVIEW_INSTANCES = 8 + +function getPreviewContent(sha: string): DocsContent { + const existing = previews.get(sha) + if (existing) { + previews.delete(sha) + previews.set(sha, existing) + return existing + } + + const instance = contentAt(sha) + previews.set(sha, instance) + + while (previews.size > MAX_PREVIEW_INSTANCES) { + const oldest = previews.keys().next() + if (oldest.done) break + const evicted = previews.get(oldest.value) + previews.delete(oldest.value) + void evicted?.dispose().catch(() => {}) + } + + return instance +} + +/** + * Serve a preview request through the instance pinned to `sha`. + */ +export async function servePreview(event: Parameters[0], sha: string, segment: string) { + const request = toWebRequest(event) + const url = new URL(request.url) + url.pathname = url.pathname.replace(segment, '') + const rewritten = new Request(url, request) + + if (!getHeadSha() || sha === getHeadSha()) { + const instance = await getProdContent() + // Recheck after promise resolves. + if (sha === getHeadSha()) return instance.handler(rewritten) + } + return getPreviewContent(sha).handler(rewritten) +} diff --git a/server/utils/timing.ts b/server/utils/timing.ts new file mode 100644 index 0000000..17a4cce --- /dev/null +++ b/server/utils/timing.ts @@ -0,0 +1,29 @@ +/** Named phase timings for one revalidate webhook run. */ +export interface Timings { + /** Time a sync or async `fn` under `label`; records its duration and returns its result. */ + time(label: string, fn: () => T | Promise): Promise + /** `label=123ms label2=45ms`, in recorded order — for one log line. */ + format(): string + /** ms since this recorder was created — spans the sync response and the background `waitUntil` phase. */ + since(): number +} + +export function createTimings(): Timings { + const start = performance.now() + const entries: { label: string; ms: number }[] = [] + + async function time(label: string, fn: () => T | Promise): Promise { + const phaseStart = performance.now() + try { + return await fn() + } finally { + entries.push({ label, ms: Math.round(performance.now() - phaseStart) }) + } + } + + function format(): string { + return entries.map(({ label, ms }) => `${label}=${ms}ms`).join(' ') + } + + return { time, format, since: () => Math.round(performance.now() - start) } +} diff --git a/server/utils/webhook.ts b/server/utils/webhook.ts new file mode 100644 index 0000000..d08b8fb --- /dev/null +++ b/server/utils/webhook.ts @@ -0,0 +1,117 @@ +import { DEFAULT_CONTENT_NAME, type ContentListFile } from 'comark-content' +import type { GitHubCommit } from './github' +import { hashManifestItem } from './json' + +/** How a push changed the content source, already filtered to `contentDir`. */ +export interface ContentChanges { + /** Manifest keys (`default/`) of files added or modified. */ + upserted: string[] + /** Manifest keys of files removed — only the previous manifest can resolve their paths. */ + removed: string[] + /** A `.navigation.*` file changed, so the tree changed regardless of which pages did. */ + navTouched: boolean +} + +/** Files the content source can actually serve — matches the parsers installed in `content.ts`. */ +const CONTENT_EXTENSIONS = ['.md', '.yml', '.yaml', '.json'] + +/** The content instance's name (see `createBaseContent()` in `content.ts`) — unnamed, so `default`. */ +const SOURCE_NAME = DEFAULT_CONTENT_NAME + +/** + * A push's changed content files, named by their manifest key (`default/`) — the + * reverse of `meta.key`, so a diff against `manifest.items` doesn't need to re-derive file → URL + * mappings that comark already owns. + */ +export function changesForPush(contentDir: string, commits: GitHubCommit[]): ContentChanges { + const upserted = new Set() + const removed = new Set() + let navTouched = false + + const consider = (file: string, into: Set) => { + const key = manifestKeyFor(file, contentDir) + if (!key) return + + if (isNavConfigFile(file)) navTouched = true + else into.add(key) + } + + for (const commit of commits) { + for (const file of commit.added ?? []) consider(file, upserted) + for (const file of commit.modified ?? []) consider(file, upserted) + for (const file of commit.removed ?? []) consider(file, removed) + } + + // A path removed and re-added in the same push is an upsert, not a removal. + for (const key of upserted) removed.delete(key) + + return { upserted: [...upserted], removed: [...removed], navTouched } +} + +/** Repo-relative path → its key in the manifest, or `null` when it can't be a content file. */ +function manifestKeyFor(file: string, contentDir: string): string | null { + const dir = contentDir.replace(/^\/+|\/+$/g, '') + const prefix = dir ? `${dir}/` : '' + + if (prefix && !file.startsWith(prefix)) return null + if (!CONTENT_EXTENSIONS.some((ext) => file.toLowerCase().endsWith(ext))) return null + + return `${SOURCE_NAME}/${file.slice(prefix.length)}` +} + +/** Directory configuration (`.navigation.yml`), which contributes to the tree rather than a page. */ +function isNavConfigFile(file: string): boolean { + return /\.navigation\.(?:ya?ml|json)$/i.test(file) +} + +/** + * The payload URL a client-side navigation fetches for `path` + */ +export function payloadUrlForPage(path: string, buildId?: string): string { + const base = path === '/' ? '/_payload.json' : `${path.replace(/\/$/, '')}/_payload.json` + return buildId ? `${base}?_b=${buildId}` : base +} + +/** `default/` (a manifest key) → page path, the reverse of what the path-keyed manifest gives. */ +export function indexByFileKey(items: Record): Map { + const index = new Map() + for (const item of Object.values(items)) index.set(item.meta.key, item.path) + return index +} + +/** + * Which pages a push changed, and whether the tree itself moved. + */ +export function diffContent( + changes: ContentChanges, + before: Record, + after: Record +): { pagePaths: string[]; navChanged: boolean } { + const pagePaths = new Set() + + const afterByKey = indexByFileKey(after) + const beforeByKey = indexByFileKey(before) + + for (const key of changes.upserted) { + const path = afterByKey.get(key) + if (path) pagePaths.add(path) + } + for (const key of changes.removed) { + const path = beforeByKey.get(key) + if (path) pagePaths.add(path) + } + + const beforeKeys = Object.keys(before) + const afterKeys = Object.keys(after) + const navChanged = + beforeKeys.length !== afterKeys.length || + afterKeys.some((key) => !before[key]) || + // Listing fields (title, description, icon, `navigation`…) are what the tree renders from. + afterKeys.some((key) => before[key] && !sameListing(before[key]!, after[key]!)) + + return { pagePaths: [...pagePaths], navChanged } +} + +function sameListing(a: ContentListFile, b: ContentListFile): boolean { + return a.path === b.path && hashManifestItem(a) === hashManifestItem(b) +} diff --git a/test/content-contract.test.ts b/test/content-contract.test.ts index 20de79d..8825ba9 100644 --- a/test/content-contract.test.ts +++ b/test/content-contract.test.ts @@ -5,12 +5,20 @@ * which is how comark-content#77's `metaOnly` -> `partial` rename silently degraded * the webhook's body warm-up. This exercises the surface the layer depends on. */ +import { mkdtemp, readFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' -import { comarkContent, defineContentPlugin } from 'comark-content' +import { comarkContent, DEFAULT_CONTENT_NAME, readArtifact } from 'comark-content' +import { writeSnapshots } from 'comark-content/build' import fsSource from 'comark-content/sources/fs' import githubSource from 'comark-content/sources/github' -import { createContentClient, defineContentClientPlugin } from 'comark-content/client' +import snapshot, { withSnapshot } from 'comark-content/sources/snapshot' +import { createContentClient } from 'comark-content/client' +import { comarkContent as runtimeComarkContent, DEFAULT_CONTENT_NAME as RUNTIME_CONTENT_NAME, readArtifact as runtimeReadArtifact } from 'comark-content/runtime' +import sqliteWasm from 'comark-content/database/sqlite-wasm' +import sqliteFullTextSearch from 'comark-content/plugins/sqlite-full-text-search' import memoryDriver from 'unstorage/drivers/memory' const fixture = fileURLToPath(new URL('./fixtures/content-contract', import.meta.url)) @@ -36,7 +44,18 @@ function createFixtureContent() { describe('comark-content contract', () => { it('exposes every entrypoint the layer imports', () => { - for (const entry of [comarkContent, defineContentPlugin, fsSource, githubSource, createContentClient, defineContentClientPlugin]) { + for (const entry of [ + comarkContent, + readArtifact, + fsSource, + githubSource, + createContentClient, + runtimeComarkContent, + runtimeReadArtifact, + // Browser-only at runtime, but the subpaths resolve under node — enough to catch a rename. + sqliteWasm, + sqliteFullTextSearch, + ]) { expect(typeof entry).toBe('function') } }) @@ -71,24 +90,130 @@ describe('comark-content contract', () => { expect(cached!.nodes.length).toBeGreaterThan(0) }) - it('dispatches plugin serve handlers through content.handler', async () => { - // Mirrors the `search-sections` plugin in server/utils/content.ts. - const ping = defineContentPlugin(() => ({ - name: 'ping', - setup(ctx) { - ctx.addServeHandler('ping', async () => Response.json({ ok: true })) - }, - })) - - const content = comarkContent({ - source: fsSource(fixture), - cache: { driver: memoryDriver() }, - plugins: [ping()], + it('serves the manifest and snapshot artifacts through content.handler', async () => { + const content = createFixtureContent() + await content.init(full) + + // The exact paths the search worker fetches and `modules/config/` declares ISR rules for. + for (const path of ['manifest.json', `snapshot/${DEFAULT_CONTENT_NAME}.json`]) { + const response = await content.handler(new Request(`http://localhost/api/content/${path}`)) + expect(response.status, path).toBe(200) + const artifact = await response.json() + expect(Object.keys(artifact), path).toContain('checksum') + expect(Object.keys(await readArtifact(artifact)).length, path).toBeGreaterThan(0) + } + }) + + it('hydrates a sourceless instance from those artifacts', async () => { + const server = createFixtureContent() + await server.init(full) + + const fetchArtifact = async (path: string) => + await (await server.handler(new Request(`http://localhost/api/content/${path}`))).json() + + // The search feature is this round-trip, so a break here is a silently empty search index. + // `snapshot()`'s first argument is the full-body tier; the second (optional) manifest tier + // lets a bare `init()` skip downloading bodies until a document is actually requested. + const client = comarkContent({ + source: snapshot( + () => fetchArtifact(`snapshot/${DEFAULT_CONTENT_NAME}.json`), + () => fetchArtifact('manifest.json') + ), }) + await client.init() - const response = await content.handler(new Request('http://localhost/api/content/ping')) + expect(Object.keys((await client.manifest()).items)).toEqual(['/']) - expect(response.status).toBe(200) - expect(await response.json()).toEqual({ ok: true }) + // Bodies have to arrive parsed: the client has no source to read a document from. + const doc = await client.get('/') + expect(doc?.data?.title).toBe('Contract fixture') + expect(doc?.nodes?.length).toBeGreaterThan(0) + }) + + it('hydrates a runtime-entry instance from those artifacts', async () => { + // `app/workers/search.ts` imports from `comark-content/runtime`, the parser-free entry. + // A snapshot-only instance must still hydrate there, and the name has to stay in step with + // the artifact paths the worker fetches. + expect(RUNTIME_CONTENT_NAME).toBe(DEFAULT_CONTENT_NAME) + + const server = createFixtureContent() + await server.init(full) + + const fetchArtifact = async (path: string) => + await (await server.handler(new Request(`http://localhost/api/content/${path}`))).json() + + const client = runtimeComarkContent({ + source: snapshot( + () => fetchArtifact(`snapshot/${RUNTIME_CONTENT_NAME}.json`), + () => fetchArtifact('manifest.json') + ), + }) + await client.init() + + expect(Object.keys((await client.manifest()).items)).toEqual(['/']) + const doc = await client.get('/') + expect(doc?.data?.title).toBe('Contract fixture') + expect(doc?.nodes?.length).toBeGreaterThan(0) + }) + + describe('build-time snapshot', () => { + /** + * `modules/snapshot/` writes it with `writeSnapshots()`, and + * `server/utils/content.ts` reads it back through a Nitro server asset. Three things here are + * layout, not behaviour, and all are silent when wrong: the per-instance subdirectory, the fact + * that a server asset hands back JSON *text*, and `manifest: false` suppressing the light tier + * the layer does not read. + */ + async function writeSnapshotFile() { + const dir = await mkdtemp(join(tmpdir(), 'comark-snapshot-')) + await writeSnapshots(createFixtureContent(), { dir, manifest: false }) + // One directory per instance, named after it — ours is unnamed, so `default`. + const read = (file: string) => readFile(join(dir, DEFAULT_CONTENT_NAME, file), 'utf8') + return { snapshot: () => read('snapshot.json'), manifest: () => read('manifest.json') } + } + + it('writes the snapshot alone when the manifest tier is off', async () => { + const stored = await writeSnapshotFile() + + await expect(stored.snapshot()).resolves.toContain('Contract fixture') + await expect(stored.manifest()).rejects.toThrow() + }) + + it('hydrates a withSnapshot instance from the snapshot without reading the source', async () => { + const stored = await writeSnapshotFile() + + // A source that throws on any read: hydrating from the snapshot must not touch it. This is the + // cold start being bought — in production the reads it stands in for are GitHub API calls. + const unreachable = { + ...fsSource(fixture), + keys: () => { + throw new Error('the origin was walked') + }, + } + + const content = comarkContent({ + source: withSnapshot(unreachable, stored.snapshot), + cache: { driver: memoryDriver() }, + }) + await content.init(full) + + expect(Object.keys((await content.manifest()).items)).toEqual(['/']) + const doc = await content.get('/') + expect(doc?.data?.title).toBe('Contract fixture') + expect(doc?.nodes?.length).toBeGreaterThan(0) + }) + + it('falls back to the source when no snapshot is stored', async () => { + // What every ref other than the build commit gets: loaders return `null`, so the origin is + // the only provider. A snapshot that cannot prove it belongs to this ref must never be used. + const content = comarkContent({ + source: withSnapshot(fsSource(fixture), () => null), + cache: { driver: memoryDriver() }, + }) + await content.init(full) + + expect(Object.keys((await content.manifest()).items)).toEqual(['/']) + expect((await content.get('/'))?.data?.title).toBe('Contract fixture') + }) }) }) diff --git a/test/geist-theme.test.ts b/test/geist.test.ts similarity index 99% rename from test/geist-theme.test.ts rename to test/geist.test.ts index a24a6e6..bcb48ab 100644 --- a/test/geist-theme.test.ts +++ b/test/geist.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { parseMarkdown } from 'comark' import rangi from 'comark/plugins/rangi' -import { geistDark, geistLight, geistTheme } from '../utils/geist-theme' +import { geistDark, geistLight, geistTheme } from '../utils/geist' describe('Geist syntax theme', () => { it('uses the live Geist light syntax roles', () => { diff --git a/test/git.test.ts b/test/git.test.ts index 54f9356..7728da7 100644 --- a/test/git.test.ts +++ b/test/git.test.ts @@ -1,6 +1,9 @@ +import { execFileSync } from 'node:child_process' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { parseGitRemote } from '../utils/git' -import { inferSiteURL } from '../utils/meta' +import { getLastCommit, getTreeSha, hasParent, parseGitRemote } from '../utils/git' describe('parseGitRemote', () => { it('parses SSH remotes', () => { @@ -31,53 +34,76 @@ describe('parseGitRemote', () => { }) }) -describe('inferSiteURL', () => { - const keys = [ - 'NUXT_PUBLIC_SITE_URL', - 'NUXT_SITE_URL', - 'VERCEL_PROJECT_PRODUCTION_URL', - 'VERCEL_BRANCH_URL', - 'VERCEL_URL', - 'URL', - 'CI_PAGES_URL', - 'CF_PAGES_URL', - ] - let saved: Record - - // `Reflect.deleteProperty` rather than `delete process.env[key]`: same effect, - // without tripping `no-dynamic-delete`. - const unset = (key: string) => Reflect.deleteProperty(process.env, key) - - beforeEach(() => { - saved = Object.fromEntries(keys.map((key) => [key, process.env[key]])) - for (const key of keys) unset(key) + +describe('commit and tree helpers', () => { + let repo: string + + const run = (...args: string[]) => execFileSync('git', args, { cwd: repo, stdio: 'ignore' }) + const write = async (file: string, body: string) => { + await mkdir(dirname(join(repo, file)), { recursive: true }) + await writeFile(join(repo, file), body, 'utf8') + } + + beforeEach(async () => { + repo = await mkdtemp(join(tmpdir(), 'comark-git-')) + run('init', '-q', '-b', 'main') + run('config', 'user.email', 'test@example.com') + run('config', 'user.name', 'Test') + + await write('content/index.md', '# one\n') + run('add', '-A') + run('commit', '-qm', 'add content') + + // A later commit that leaves `content/` untouched, so HEAD is not the last content commit. + await write('src/app.ts', 'export const a = 1\n') + run('add', '-A') + run('commit', '-qm', 'add code') }) - afterEach(() => { - for (const [key, value] of Object.entries(saved)) { - if (value === undefined) unset(key) - else process.env[key] = value - } + afterEach(async () => { + await rm(repo, { recursive: true, force: true }) }) - it('returns undefined when nothing is set', () => { - expect(inferSiteURL()).toBeUndefined() + it('finds the last commit touching a directory, not HEAD', () => { + const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim() + const last = getLastCommit(repo, 'content') + + expect(last).toMatch(/^[0-9a-f]{40}$/) + expect(last).not.toBe(head) }) - it('adds https to a bare Vercel host', () => { - process.env.VERCEL_URL = 'my-app-abc123.vercel.app' - expect(inferSiteURL()).toBe('https://my-app-abc123.vercel.app') + it('returns the same tree for a ref whose content matches HEAD', () => { + // The whole safety property of the build-time snapshot: the commit it is labelled with has to hold + // the content that was parsed. Here the code commit did not touch `content/`, so both agree. + const last = getLastCommit(repo, 'content')! + expect(getTreeSha(repo, last, 'content')).toBe(getTreeSha(repo, 'HEAD', 'content')) }) - it('prefers the explicit override over the platform value', () => { - process.env.VERCEL_URL = 'my-app-abc123.vercel.app' - process.env.NUXT_PUBLIC_SITE_URL = 'https://docs.example.com' - expect(inferSiteURL()).toBe('https://docs.example.com') + it('returns a different tree once the content changes', async () => { + const before = getTreeSha(repo, 'HEAD', 'content')! + + await write('content/index.md', '# two\n') + run('add', '-A') + run('commit', '-qm', 'edit content') + + expect(getTreeSha(repo, 'HEAD', 'content')).not.toBe(before) + // A stale label is what the snapshot must never be written under. + expect(getTreeSha(repo, 'HEAD~1', 'content')).toBe(before) }) - it('prefers the production URL over the per-branch one', () => { - process.env.VERCEL_BRANCH_URL = 'branch.vercel.app' - process.env.VERCEL_PROJECT_PRODUCTION_URL = 'docs.comark.dev' - expect(inferSiteURL()).toBe('https://docs.comark.dev') + it('returns undefined for a ref or path outside the checkout', () => { + expect(getTreeSha(repo, 'HEAD', 'nope')).toBeUndefined() + expect(getTreeSha(repo, 'a'.repeat(40), 'content')).toBeUndefined() + expect(getLastCommit(repo, 'nope')).toBeUndefined() + }) + + it('reports a missing parent at the root commit', () => { + const root = execFileSync('git', ['rev-list', '--max-parents=0', 'HEAD'], { + cwd: repo, + encoding: 'utf8', + }).trim() + + expect(hasParent(repo, 'HEAD')).toBe(true) + expect(hasParent(repo, root)).toBe(false) }) }) diff --git a/test/github.test.ts b/test/github.test.ts index b39103d..24c4904 100644 --- a/test/github.test.ts +++ b/test/github.test.ts @@ -1,6 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { resolveContentSha } from '../server/utils/github' +/** One commits-query response. `resolveContentSha` reads only `sha`. */ +const commits = (sha: string) => new Response(JSON.stringify([{ sha }]), { status: 200 }) + afterEach(() => { vi.unstubAllEnvs() vi.unstubAllGlobals() @@ -8,21 +11,23 @@ afterEach(() => { describe('resolveContentSha', () => { it('resolves the latest commit touching the configured content directory', async () => { - const fetch = vi.fn().mockResolvedValue([{ sha: 'content-sha' }]) - vi.stubGlobal('$fetch', fetch) + const fetch = vi.fn().mockResolvedValue(commits('content-sha')) + vi.stubGlobal('fetch', fetch) await expect(resolveContentSha('feat/docs', '/docs/content/')).resolves.toBe('content-sha') - expect(fetch).toHaveBeenCalledWith( - 'https://api.github.com/repos/comarkdown/comark-docs/commits', - expect.objectContaining({ - query: { sha: 'feat/docs', path: 'docs/content', per_page: 1 }, - }) - ) + + const requested = new URL(String(fetch.mock.calls[0]![0])) + expect(requested.pathname).toBe('/repos/comarkdown/comark-docs/commits') + expect(Object.fromEntries(requested.searchParams)).toEqual({ + sha: 'feat/docs', + path: 'docs/content', + per_page: '1', + }) }) it('caches each branch and content directory independently', async () => { - const fetch = vi.fn().mockResolvedValueOnce([{ sha: 'docs-sha' }]).mockResolvedValueOnce([{ sha: 'api-sha' }]) - vi.stubGlobal('$fetch', fetch) + const fetch = vi.fn().mockResolvedValueOnce(commits('docs-sha')).mockResolvedValueOnce(commits('api-sha')) + vi.stubGlobal('fetch', fetch) await expect(resolveContentSha('test/cache-key', 'docs/content')).resolves.toBe('docs-sha') await expect(resolveContentSha('test/cache-key', 'docs/content')).resolves.toBe('docs-sha') @@ -31,8 +36,8 @@ describe('resolveContentSha', () => { }) it('can refresh a cached content revision for the push webhook', async () => { - const fetch = vi.fn().mockResolvedValueOnce([{ sha: 'before' }]).mockResolvedValueOnce([{ sha: 'after' }]) - vi.stubGlobal('$fetch', fetch) + const fetch = vi.fn().mockResolvedValueOnce(commits('before')).mockResolvedValueOnce(commits('after')) + vi.stubGlobal('fetch', fetch) await expect(resolveContentSha('test/refresh', 'docs/content')).resolves.toBe('before') await expect(resolveContentSha('test/refresh', 'docs/content')).resolves.toBe('before') diff --git a/test/paths.test.ts b/test/paths.test.ts index e8c96bb..36f36e9 100644 --- a/test/paths.test.ts +++ b/test/paths.test.ts @@ -1,14 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { resetRuntimeConfig, setRuntimeConfig } from './setup' -import { - contentPrefix, - isContentMd, - isNavConfig, - pageUrlForPath, - payloadUrlForRoute, - rawUrlForPath, - slugFromPath, -} from '../server/utils/paths' +import { contentPrefix } from '../server/utils/paths' afterEach(resetRuntimeConfig) @@ -19,79 +11,3 @@ describe('contentPrefix', () => { expect(contentPrefix()).toBe('docs/content/') }) }) - -describe('isContentMd', () => { - it('matches markdown under the content dir only', () => { - expect(isContentMd('content/index.md')).toBe(true) - expect(isContentMd('content/1.guide/2.intro.MD')).toBe(true) - expect(isContentMd('content/.navigation.yml')).toBe(false) - expect(isContentMd('README.md')).toBe(false) - expect(isContentMd('other/content/x.md')).toBe(false) - }) - - it('follows a nested content dir', () => { - setRuntimeConfig({ contentDir: 'docs/content' }) - expect(isContentMd('docs/content/x.md')).toBe(true) - expect(isContentMd('content/x.md')).toBe(false) - }) -}) - -describe('isNavConfig', () => { - it('matches the yml/yaml/json navigation files', () => { - expect(isNavConfig('content/1.guide/.navigation.yml')).toBe(true) - expect(isNavConfig('content/.navigation.yaml')).toBe(true) - expect(isNavConfig('content/.navigation.json')).toBe(true) - expect(isNavConfig('content/navigation.yml')).toBe(false) - expect(isNavConfig('content/x.md')).toBe(false) - }) -}) - -describe('slugFromPath', () => { - it('strips numeric ordering prefixes at every level', () => { - expect(slugFromPath('content/1.getting-started/2.intro.md')).toEqual({ - isIndex: false, - segments: ['getting-started', 'intro'], - }) - }) - - it('treats index files as their parent', () => { - expect(slugFromPath('content/index.md')).toEqual({ isIndex: true, segments: [] }) - expect(slugFromPath('content/1.guide/index.md')).toEqual({ isIndex: true, segments: ['guide'] }) - }) - - it('returns null for anything outside the content dir', () => { - expect(slugFromPath('README.md')).toBeNull() - expect(slugFromPath('content/.navigation.yml')).toBeNull() - }) -}) - -describe('pageUrlForPath', () => { - it('maps content files to page routes', () => { - expect(pageUrlForPath('content/index.md')).toBe('/') - expect(pageUrlForPath('content/1.guide/index.md')).toBe('/guide') - expect(pageUrlForPath('content/1.guide/2.intro.md')).toBe('/guide/intro') - expect(pageUrlForPath('content/x.yml')).toBeNull() - }) -}) - -describe('rawUrlForPath', () => { - it('maps content files to their raw markdown mirror', () => { - expect(rawUrlForPath('content/index.md')).toBe('/raw/index.md') - expect(rawUrlForPath('content/1.guide/2.intro.md')).toBe('/raw/guide/intro.md') - expect(rawUrlForPath('content/1.guide/index.md')).toBe('/raw/guide.md') - expect(rawUrlForPath('README.md')).toBeNull() - }) -}) - -describe('payloadUrlForRoute', () => { - it('builds the payload URL the browser actually requests', () => { - expect(payloadUrlForRoute('/')).toBe('/_payload.json') - expect(payloadUrlForRoute('/guide/intro')).toBe('/guide/intro/_payload.json') - }) - - it('appends the build id when there is one', () => { - // The webhook has to purge the exact keyed URL, not the bare path. - expect(payloadUrlForRoute('/guide', 'abc123')).toBe('/guide/_payload.json?abc123') - expect(payloadUrlForRoute('/', 'abc123')).toBe('/_payload.json?abc123') - }) -}) diff --git a/test/setup.ts b/test/setup.ts index ba5873c..d5a4cde 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,7 +1,7 @@ // Nitro auto-imports, provided by hand: modules under `server/` are written against Nitro's globals, so // importing one directly in a test leaves those names undefined. Declaring the few the tests touch here beats // pulling in the whole Nuxt/Nitro harness for a handful of pure functions. `useRuntimeConfig` returns the shape -// `modules/config.ts` seeds. +// `modules/config/` seeds. import memoryDriver from 'unstorage/drivers/memory' export interface TestRuntimeConfig { diff --git a/test/webhook.test.ts b/test/webhook.test.ts new file mode 100644 index 0000000..25cc85f --- /dev/null +++ b/test/webhook.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from 'vitest' +import type { ContentListFile } from 'comark-content' +import type { GitHubCommit } from '../server/utils/github' +import { changesForPush, diffContent, indexByFileKey, payloadUrlForPage } from '../server/utils/webhook' + +const commit = (partial: GitHubCommit): GitHubCommit => partial + +describe('changesForPush', () => { + it('classifies added/modified/removed content files, keyed by their manifest key', () => { + const commits = [ + commit({ + added: ['content/1.guide/2.intro.md'], + modified: ['content/index.md'], + removed: ['content/old.md'], + }), + ] + expect(changesForPush('content', commits)).toEqual({ + upserted: ['default/1.guide/2.intro.md', 'default/index.md'], + removed: ['default/old.md'], + navTouched: false, + }) + }) + + it('ignores files outside the content dir', () => { + expect(changesForPush('content', [commit({ modified: ['README.md', 'other/content/x.md'] })])).toEqual({ + upserted: [], + removed: [], + navTouched: false, + }) + }) + + it('follows a nested content dir', () => { + expect(changesForPush('docs/content', [commit({ modified: ['docs/content/x.md'] })])).toEqual({ + upserted: ['default/x.md'], + removed: [], + navTouched: false, + }) + }) + + it('covers every parser extension, not just markdown', () => { + const commits = [commit({ added: ['content/data.yml', 'content/data.yaml', 'content/data.json'] })] + expect(changesForPush('content', commits).upserted).toEqual([ + 'default/data.yml', + 'default/data.yaml', + 'default/data.json', + ]) + }) + + it('flags a navigation config file instead of collecting it', () => { + const commits = [commit({ modified: ['content/1.guide/.navigation.yml'] })] + expect(changesForPush('content', commits)).toEqual({ upserted: [], removed: [], navTouched: true }) + }) + + it('treats a path removed and re-added in the same push as an upsert', () => { + const commits = [commit({ added: ['content/index.md'], removed: ['content/index.md'] })] + expect(changesForPush('content', commits)).toEqual({ + upserted: ['default/index.md'], + removed: [], + navTouched: false, + }) + }) +}) + +describe('payloadUrlForPage', () => { + it('matches the `_b` query param Nuxt requests (`nuxt/dist/app/composables/payload.js`)', () => { + expect(payloadUrlForPage('/')).toBe('/_payload.json') + expect(payloadUrlForPage('/guide/intro')).toBe('/guide/intro/_payload.json') + expect(payloadUrlForPage('/guide', 'abc123')).toBe('/guide/_payload.json?_b=abc123') + expect(payloadUrlForPage('/', 'abc123')).toBe('/_payload.json?_b=abc123') + }) +}) + +describe('indexByFileKey', () => { + it('maps a manifest key back to its page path', () => { + const items: Record = { + '/guide/intro': { path: '/guide/intro', data: {}, meta: { key: 'content/1.guide/2.intro.md' } } as never, + } + expect(indexByFileKey(items).get('content/1.guide/2.intro.md')).toBe('/guide/intro') + }) +}) + +describe('diffContent', () => { + const file = (path: string, key: string, data: Record = {}): ContentListFile => + ({ path, data, meta: { key } }) as never + + it('resolves upserted/removed manifest keys to page paths', () => { + const before = { '/old': file('/old', 'content/old.md') } + const after = { '/guide/intro': file('/guide/intro', 'content/1.guide/2.intro.md') } + const changes = { upserted: ['content/1.guide/2.intro.md'], removed: ['content/old.md'], navTouched: false } + expect(diffContent(changes, before, after).pagePaths.sort()).toEqual(['/guide/intro', '/old']) + }) + + it('flags navChanged when a page is added or removed', () => { + const before = { '/a': file('/a', 'content/a.md') } + const after = { '/a': file('/a', 'content/a.md'), '/b': file('/b', 'content/b.md') } + expect(diffContent({ upserted: [], removed: [], navTouched: false }, before, after).navChanged).toBe(true) + }) + + it('flags navChanged when listing data changes, even with the same page set', () => { + const before = { '/a': file('/a', 'content/a.md', { title: 'A' }) } + const after = { '/a': file('/a', 'content/a.md', { title: 'B' }) } + expect(diffContent({ upserted: [], removed: [], navTouched: false }, before, after).navChanged).toBe(true) + }) + + it('does not flag navChanged when nothing listing-relevant moved', () => { + const before = { '/a': file('/a', 'content/a.md', { title: 'A' }) } + const after = { '/a': file('/a', 'content/a.md', { title: 'A' }) } + expect(diffContent({ upserted: ['content/a.md'], removed: [], navTouched: false }, before, after).navChanged).toBe( + false + ) + }) +}) diff --git a/utils/content.ts b/utils/content.ts new file mode 100644 index 0000000..120ac52 --- /dev/null +++ b/utils/content.ts @@ -0,0 +1,60 @@ +import type { Tracer } from '@opentelemetry/api' +import { type ContentOptions, comarkContent } from 'comark-content' +import markdown from 'comark-content/plugins/markdown' +import yaml from 'comark-content/plugins/yaml' +import tracingOtel from 'comark-content/plugins/tracing/otel' +import rangi from 'comark/plugins/rangi' +import security from 'comark/plugins/security' +import emoji from 'comark/plugins/emoji' +import toc from 'comark/plugins/toc' +import mermaid from 'comark/plugins/mermaid' +import { geistTheme } from './geist.ts' +import { contentTracer } from '../server/utils/tracer.ts' + +/** Frontmatter kept in the manifest, so `list()` and `navigation()` render without reading bodies. */ +const LISTING_FIELDS = ['title', 'description', 'navigation', 'icon', 'layout'] + +// Bump CONTENT_PARSER_VERSION in `server/utils/cache.ts` when these plugins or their options change cached output. +const comarkPlugins = [ + mermaid({ theme: 'zinc-light', themeDark: 'zinc-dark' }), + rangi({ theme: geistTheme }), + toc({ depth: 3 }), + emoji(), + security({ + blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style'], + allowDataImages: false, + }), +] + +/** + * The parser, in one place for: + * - The build-time snapshot + * - The runtime instance + */ +function create(options: Pick, tracer?: Tracer) { + return comarkContent({ + source: options.source, + plugins: [ + markdown({ + comark: { plugins: comarkPlugins }, + listingFields: LISTING_FIELDS, + }), + yaml({ listingFields: LISTING_FIELDS }), + tracer && tracingOtel({ tracer }), + ], + cache: options.cache, + basePath: options.basePath, + }) +} + +/** An instance serving requests: traced, and cached per content SHA. */ +export function createRuntimeContentInstance(options: Pick) { + return create(options, contentTracer()) +} + +/** + * The throwaway instance the build-time snapshot is parsed with (`modules/snapshot/`). + */ +export function createBuildContentInstance(options: Pick) { + return create(options) +} diff --git a/utils/geist-theme.ts b/utils/geist.ts similarity index 100% rename from utils/geist-theme.ts rename to utils/geist.ts diff --git a/utils/git.ts b/utils/git.ts index ad1a7ba..e323fa6 100644 --- a/utils/git.ts +++ b/utils/git.ts @@ -1,4 +1,4 @@ -import { execSync } from 'node:child_process' +import { execFileSync } from 'node:child_process' export interface GitInfo { name: string @@ -6,11 +6,10 @@ export interface GitInfo { url: string } -function git(command: string, cwd: string): string | undefined { +/** Run git with an argv array — no shell, so paths with spaces need no quoting. */ +function git(args: string[], cwd: string): string | undefined { try { - return execSync(`git ${command}`, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }) - .toString() - .trim() + return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch { return undefined } @@ -27,19 +26,19 @@ export function getGitBranch(cwd: string): string { if (envName && envName !== 'HEAD') return envName - const branch = git('rev-parse --abbrev-ref HEAD', cwd) + const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd) return branch && branch !== 'HEAD' ? branch : 'main' } /** Absolute path of the git repository root containing `cwd`, if any. */ export function getGitRoot(cwd: string): string | undefined { - return git('rev-parse --show-toplevel', cwd) + return git(['rev-parse', '--show-toplevel'], cwd) } /** * Owner/name/url from a git remote URL, in both forms `git remote get-url` emits (`git@host:owner/name.git`, * `https://host/owner/name(.git)`). Split out from `getLocalGitInfo` so the regex is testable without a - * checkout — every inferred default in `modules/config.ts` (site name, edit links, webhook repo) flows from it. + * checkout — every inferred default in `modules/config/` (site name, edit links, webhook repo) flows from it. */ export function parseGitRemote(remote: string): GitInfo | undefined { const match = remote.trim().match(/^(?:git@|https?:\/\/)([^/:]+)[/:]([^/]+)\/(.+?)(?:\.git)?$/) @@ -51,7 +50,7 @@ export function parseGitRemote(remote: string): GitInfo | undefined { /** Owner/name/url parsed from the `origin` remote of the local checkout. */ export function getLocalGitInfo(cwd: string): GitInfo | undefined { - const remote = git('remote get-url origin', cwd) + const remote = git(['remote', 'get-url', 'origin'], cwd) return remote ? parseGitRemote(remote) : undefined } @@ -74,3 +73,32 @@ export function getGitEnv(): GitInfo | undefined { return { name, owner, url: `https://${provider || 'github'}.com/${owner}/${name}` } } + +/** + * The last commit touching `dir`, or `undefined`. + * + * Unverified on purpose. CI clones shallowly, and when the last commit touching `dir` predates the + * fetched window git answers with the shallow boundary commit rather than nothing — at depth 1, + * that is HEAD for every path. Confirm the answer with {@link getTreeSha} before trusting it to + * name a commit's content. + */ +export function getLastCommit(cwd: string, dir: string): string | undefined { + const sha = git(['log', '-1', '--format=%H', '--', dir], cwd) + return sha && /^[0-9a-f]{40}$/.test(sha) ? sha : undefined +} + +/** Tree object id of `:`, or `undefined` when the ref or the path is not in this checkout. */ +export function getTreeSha(cwd: string, ref: string, dir: string): string | undefined { + return git(['rev-parse', `${ref}:${dir}`], cwd) +} + +/** Whether `ref` has a parent in this checkout. `false` at a shallow-clone boundary. */ +export function hasParent(cwd: string, ref: string): boolean { + return Boolean(git(['rev-parse', '--verify', `${ref}^`], cwd)) +} + +/** The commit checked out here, falling back to the CI-provided one. */ +export function headCommit(cwd: string): string | undefined { + const sha = git(['rev-parse', 'HEAD'], cwd) || process.env.VERCEL_GIT_COMMIT_SHA + return sha && /^[0-9a-f]{40}$/.test(sha) ? sha : undefined +} diff --git a/utils/github.ts b/utils/github.ts new file mode 100644 index 0000000..24cfec0 --- /dev/null +++ b/utils/github.ts @@ -0,0 +1,36 @@ +export interface LastContentCommitOptions { + /** `owner/name` of the content repository. */ + repo: string + /** Content directory; leading and trailing slashes are trimmed. */ + path: string + /** Branch or commit to walk history from. */ + ref: string + token?: string +} + +/** + * The last commit reachable from `ref` that touched `path`. + */ +export async function fetchLastContentCommit(opts: LastContentCommitOptions): Promise { + const query = new URLSearchParams({ + sha: opts.ref, + path: opts.path.replace(/^\/+|\/+$/g, ''), + per_page: '1', + }) + + const response = await fetch(`https://api.github.com/repos/${opts.repo}/commits?${query}`, { + headers: { + Accept: 'application/vnd.github+json', + ...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}), + }, + }) + + if (!response.ok) { + throw Object.assign(new Error(`GitHub commits query failed with ${response.status}`), { + statusCode: response.status, + }) + } + + const commits = (await response.json()) as Array<{ sha?: string }> + return commits[0]?.sha +} diff --git a/utils/icons.ts b/utils/icons.ts index 4c3d517..88e761e 100644 --- a/utils/icons.ts +++ b/utils/icons.ts @@ -1,33 +1,19 @@ -import { readFileSync } from 'node:fs' import { resolveModulePath } from 'exsolve' -// Icon collections this layer's components draw from: `lucide` (UI affordances), `simple-icons` (brand marks), -// `vscode-icons` (the file-type icons Nuxt UI's `CodeIcon` derives from a filename). -export const LAYER_ICON_COLLECTIONS = ['lucide', 'simple-icons', 'vscode-icons'] - -/** Parsed collections, memoized — the client-bundle template regenerates in dev. */ -let cache: IconifyJSONish[] | undefined - -/** The shape `@nuxt/icon` accepts in `customCollections` (a raw `IconifyJSON`). */ -interface IconifyJSONish { - prefix: string - icons: Record - [key: string]: unknown -} +// Collections served by this layer's `/api/_nuxt_icon` endpoint. +// - `lucide`: UI affordances. +// - `simple-icons`, `logos`: brand marks, mostly reached from consumer content. +// - `vscode-icons`: the file-type icons Nuxt UI's `CodeIcon` derives from a filename. +// - `unjs`: icons from UnJS's `unjs/icons` repository. +// - `logos`: icons from logos. +export const LAYER_ICON_COLLECTIONS = ['lucide', 'simple-icons', 'vscode-icons', 'logos', 'unjs'] /** - * Load the layer's icon collections as data, resolved from the layer itself. - * - * `@nuxt/icon` discovers `@iconify-json/*` only by walking `node_modules/@iconify-json` up from the consuming - * app's `rootDir`/`workspaceDir` (`modulesDir` is never consulted — see `getResolvePaths`). These packages are - * dependencies of *this layer*, so under a non-hoisting install (pnpm's default `isolated` linker) they sit in - * the virtual store, out of reach: zero collections, an empty client bundle, every icon fetched from - * api.iconify.design at runtime. Done unconditionally, so behaviour is the same however the layer is consumed. + * Nitro aliases pinning each collection to the copy installed beside this layer. */ -export function layerIconCollections(): IconifyJSONish[] { - cache ??= LAYER_ICON_COLLECTIONS.map((prefix) => { - const path = resolveModulePath(`@iconify-json/${prefix}/icons.json`, { from: import.meta.url }) - return JSON.parse(readFileSync(path, 'utf8')) as IconifyJSONish - }) - return cache +export function layerIconAliases(): Record { + return Object.fromEntries(LAYER_ICON_COLLECTIONS.map((prefix) => { + const id = `@iconify-json/${prefix}/icons.json` + return [id, resolveModulePath(id, { from: import.meta.url })] + })) } diff --git a/utils/meta.ts b/utils/meta.ts deleted file mode 100644 index fbdd97e..0000000 --- a/utils/meta.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { readFile } from 'node:fs/promises' -import { resolve } from 'pathe' -import { withHttps } from 'ufo' - -/** Infer the public site URL from the deployment platform env. */ -export function inferSiteURL(): string | undefined { - // https://github.com/unjs/std-env/issues/59 - const url = - process.env.NUXT_PUBLIC_SITE_URL || - process.env.NUXT_SITE_URL || - process.env.VERCEL_PROJECT_PRODUCTION_URL || - process.env.VERCEL_BRANCH_URL || - process.env.VERCEL_URL || - process.env.URL || // Netlify - process.env.CI_PAGES_URL || // GitLab Pages - process.env.CF_PAGES_URL // Cloudflare Pages - - return url ? withHttps(url) : undefined -} - -export async function getPackageJsonMetadata( - dir: string -): Promise<{ name?: string; description?: string; version?: string }> { - try { - const parsed = JSON.parse(await readFile(resolve(dir, 'package.json'), 'utf-8')) - return { name: parsed.name, description: parsed.description, version: parsed.version } - } catch { - return {} - } -} diff --git a/utils/first-leaf.ts b/utils/navigation.ts similarity index 100% rename from utils/first-leaf.ts rename to utils/navigation.ts