diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24952599..86ab9627 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,3 +46,9 @@ jobs: - name: Tests run: pnpm run test + + - name: Docs + run: pnpm run docs:build + + - name: Generated API reference is up to date + run: git diff --exit-code packages/docs/api diff --git a/README.md b/README.md index b3fec3a0..d3427bf2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ In this monorepository: | Package | Description | |---------|-------------| |[@vue/apollo-composable](./packages/vue-apollo-composable) |Composition API| +|[@vue/apollo-components](./packages/vue-apollo-components) |Components API, built on the composables| ## Special Sponsor diff --git a/eslint.config.ts b/eslint.config.ts index afa8ad84..7b9baa5f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -13,6 +13,8 @@ export default antfu( 'ts/no-namespace': 'off', 'ts/no-empty-object-type': 'off', 'import/first': 'off', + 'vue/attribute-hyphenation': ['error', 'never'], + 'vue/v-on-event-hyphenation': ['error', 'never'], }, }, { diff --git a/packages/docs/.vitepress/apiFlavors.ts b/packages/docs/.vitepress/apiFlavors.ts new file mode 100644 index 00000000..ef7f8156 --- /dev/null +++ b/packages/docs/.vitepress/apiFlavors.ts @@ -0,0 +1,14 @@ +export const API_FLAVORS = [ + { value: 'composition', label: 'Composition API' }, + { value: 'components', label: 'Components API' }, +] as const + +export type ApiFlavor = typeof API_FLAVORS[number]['value'] + +export const DEFAULT_FLAVOR: ApiFlavor = 'composition' + +export const STORAGE_KEY = 'vue-apollo:api-flavor' + +export function isApiFlavor(value: unknown): value is ApiFlavor { + return API_FLAVORS.some(flavor => flavor.value === value) +} diff --git a/packages/docs/.vitepress/config.ts b/packages/docs/.vitepress/config.ts index 5e098b64..a031048f 100644 --- a/packages/docs/.vitepress/config.ts +++ b/packages/docs/.vitepress/config.ts @@ -1,6 +1,33 @@ import { transformerTwoslash } from '@shikijs/vitepress-twoslash' +import container from 'markdown-it-container' import { defineConfig } from 'vitepress' import typedocSidebar from '../api/composable/typedoc-sidebar.json' +import { API_FLAVORS, DEFAULT_FLAVOR, STORAGE_KEY } from './apiFlavors.ts' + +/** + * Applies the stored flavor before first paint. + * + * Runs from `head`, so it beats hydration: without it every page would render the default + * flavor and then visibly swap for anyone who picked another one. + */ +const flavorScript = ` +try { + var f = localStorage.getItem(${JSON.stringify(STORAGE_KEY)}) || ${JSON.stringify(DEFAULT_FLAVOR)} + if (${JSON.stringify(API_FLAVORS.map(flavor => flavor.value))}.indexOf(f) !== -1) { + document.documentElement.classList.add('api-pref-' + f) + } +} catch (e) {} +`.trim() + +/** Generated so `apiFlavors.ts` stays the only place a flavor is declared. */ +const flavorStyle = [ + `html:not([class*='api-pref-']) .api-flavor--${DEFAULT_FLAVOR} { display: block }`, + ...API_FLAVORS.flatMap(({ value }, index) => [ + `html.api-pref-${value} { --api-flavor-index: ${index} }`, + `html.api-pref-${value} .api-flavor--${value} { display: block }`, + `html.api-pref-${value} .api-preference__option[data-flavor='${value}'] { color: var(--vp-c-brand-1) }`, + ]), +].join('\n') // Shared sidebar for guide sections const guideSidebar = [ @@ -79,6 +106,7 @@ const guideSidebar = [ { text: 'What\'s changed in v5', link: '/migration/whats-changed' }, { text: 'Migration guide', link: '/migration/guide' }, { text: 'Compat layer', link: '/migration/compat' }, + { text: 'Components', link: '/migration/components' }, ], }, ] @@ -89,10 +117,48 @@ export default defineConfig({ description: 'Apollo/GraphQL integration for VueJS', markdown: { codeTransformers: [ - transformerTwoslash() as any, + transformerTwoslash({ + twoslashOptions: { + /* + * Drops one diagnostic from generated code the reader never sees. + * + * A template whose only root is one of our generic SFCs makes Vue language tools + * read `$el` off that component's instance type, which under twoslash's setup + * does not carry `ComponentPublicInstance`. `vue-tsc` checks the same examples + * cleanly, so the example itself is fine. + * + * `filterNode` runs before error validation, so the node is gone rather than + * merely expected. Scoped to this message so real 2339s still fail the build. + */ + filterNode(node) { + return !(node.type === 'error' && node.code === 2339 && node.text.includes('\'$el\'')) + }, + }, + }) as any, ], + config(md) { + /* + * One container per flavor, `:::: composition-api` to `::::`, shown or hidden by CSS. + * + * Written with four colons rather than three so a flavor block can wrap the + * three-colon containers (`code-group`, `tip`, `warning`) it usually needs to. + * markdown-it-container only nests when the outer marker is the longer one. + */ + for (const { value } of API_FLAVORS) { + md.use(container, `${value}-api`, { + render: (tokens: { nesting: number }[], index: number) => + tokens[index].nesting === 1 + ? `
\n` + : '
\n', + }) + } + }, }, - head: [['link', { rel: 'icon', href: '/favicon.png' }]], + head: [ + ['link', { rel: 'icon', href: '/favicon.png' }], + ['script', {}, flavorScript], + ['style', {}, flavorStyle], + ], themeConfig: { socialLinks: [{ icon: 'github', link: 'https://github.com/vuejs/apollo' }], footer: { @@ -106,7 +172,14 @@ export default defineConfig({ nav: [ { text: 'Home', link: '/' }, { text: 'Guide', link: '/guide/' }, - { text: 'API Reference', link: '/api/composable/' }, + { + text: 'API Reference', + items: [ + { text: 'Overview', link: '/api/' }, + { text: '@vue/apollo-composable', link: '/api/composable/' }, + { text: '@vue/apollo-components', link: '/api/components/' }, + ], + }, { text: 'Sponsor', link: 'https://github.com/sponsors/Akryum', @@ -123,6 +196,16 @@ export default defineConfig({ '/networking/': guideSidebar, '/ssr/': guideSidebar, '/migration/': guideSidebar, + '/api/': [ + { + text: 'API Reference', + link: '/api/', + items: [ + { text: '@vue/apollo-composable', link: '/api/composable/' }, + { text: '@vue/apollo-components', link: '/api/components/' }, + ], + }, + ], '/api/composable/': [ { text: '@vue/apollo-composable', @@ -130,6 +213,19 @@ export default defineConfig({ items: typedocSidebar, }, ], + '/api/components/': [ + { + text: '@vue/apollo-components', + link: '/api/components/', + items: [ + { text: 'ApolloQuery', link: '/api/components/ApolloQuery' }, + { text: 'ApolloMutation', link: '/api/components/ApolloMutation' }, + { text: 'ApolloSubscription', link: '/api/components/ApolloSubscription' }, + { text: 'ApolloSubscribeToMore', link: '/api/components/ApolloSubscribeToMore' }, + { text: 'ApolloFragment', link: '/api/components/ApolloFragment' }, + ], + }, + ], }, search: { provider: 'local', diff --git a/packages/docs/.vitepress/theme/components/ApiPreference.vue b/packages/docs/.vitepress/theme/components/ApiPreference.vue new file mode 100644 index 00000000..af11c0e4 --- /dev/null +++ b/packages/docs/.vitepress/theme/components/ApiPreference.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/docs/.vitepress/theme/composables/useApiFlavor.ts b/packages/docs/.vitepress/theme/composables/useApiFlavor.ts new file mode 100644 index 00000000..8c5467d7 --- /dev/null +++ b/packages/docs/.vitepress/theme/composables/useApiFlavor.ts @@ -0,0 +1,28 @@ +import type { ApiFlavor } from '../../apiFlavors.ts' +import { createSharedComposable, useLocalStorage } from '@vueuse/core' +import { watch } from 'vue' +import { API_FLAVORS, DEFAULT_FLAVOR, isApiFlavor, STORAGE_KEY } from '../../apiFlavors.ts' + +/** Nothing rendered depends on this. It drives `aria-checked` and the click handler. */ +function useApiFlavorState() { + const flavor = useLocalStorage(STORAGE_KEY, DEFAULT_FLAVOR, { + // Read after hydration, so the first client render matches the server's default. + initOnMounted: true, + listenToStorageChanges: false, + serializer: { + read: raw => (isApiFlavor(raw) ? raw : DEFAULT_FLAVOR), + write: value => value, + }, + }) + + // Deliberately not `immediate`: the `head` script already applied the stored flavor + watch(flavor, (value) => { + for (const { value: candidate } of API_FLAVORS) { + document.documentElement.classList.toggle(`api-pref-${candidate}`, candidate === value) + } + }) + + return flavor +} + +export const useApiFlavor = createSharedComposable(useApiFlavorState) diff --git a/packages/docs/.vitepress/theme/index.ts b/packages/docs/.vitepress/theme/index.ts index f1ced207..e02a6e8c 100644 --- a/packages/docs/.vitepress/theme/index.ts +++ b/packages/docs/.vitepress/theme/index.ts @@ -1,6 +1,8 @@ import type { EnhanceAppContext } from 'vitepress' import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' import Theme from 'vitepress/theme' +import { h } from 'vue' +import ApiPreference from './components/ApiPreference.vue' import SponsorButton from './components/SponsorButton.vue' import '@shikijs/vitepress-twoslash/style.css' @@ -8,6 +10,9 @@ import './styles/index.css' export default { extends: Theme, + Layout: () => h(Theme.Layout, null, { + 'sidebar-nav-before': () => h(ApiPreference), + }), enhanceApp({ app }: EnhanceAppContext) { app.use(TwoslashFloatingVue) app.component('SponsorButton', SponsorButton) diff --git a/packages/docs/.vitepress/theme/styles/api-flavor.css b/packages/docs/.vitepress/theme/styles/api-flavor.css new file mode 100644 index 00000000..e176619c --- /dev/null +++ b/packages/docs/.vitepress/theme/styles/api-flavor.css @@ -0,0 +1,70 @@ +/* + * Which flavor is visible is decided by the `api-pref-*` class a `head` script puts on + * before first paint. Those per-flavor rules are generated from `apiFlavors.ts` in + * `config.ts`, so adding a flavor needs no change here. + */ + +.api-flavor { + display: none; + + & > :first-child { + margin-top: 0; + } +} + +.api-preference { + margin-bottom: 16px; + padding-top: 16px; + padding-bottom: 16px; + border-bottom: 1px solid var(--vp-c-divider); +} + +.api-preference__group { + position: relative; + display: grid; + padding: 3px; + border-radius: 8px; + background-color: var(--vp-c-default-soft); + + & .api-preference__thumb { + position: absolute; + top: 3px; + right: 3px; + left: 3px; + height: calc((100% - 6px) / var(--api-flavor-count, 2)); + border-radius: 6px; + background-color: var(--vp-c-bg); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.09); + transform: translateY(calc(100% * var(--api-flavor-index, 0))); + transition: transform 0.25s ease; + + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + + & .api-preference__option { + position: relative; + padding: 5px 10px; + border: 0; + border-radius: 6px; + background-color: transparent; + font-size: 12px; + font-weight: 600; + line-height: 20px; + text-align: left; + white-space: nowrap; + color: var(--vp-c-text-2); + cursor: pointer; + transition: color 0.25s; + + &:hover { + color: var(--vp-c-text-1); + } + + &:focus-visible { + outline: 2px solid var(--vp-c-brand-1); + outline-offset: 2px; + } + } +} diff --git a/packages/docs/.vitepress/theme/styles/index.css b/packages/docs/.vitepress/theme/styles/index.css index 823d6172..44593e4b 100644 --- a/packages/docs/.vitepress/theme/styles/index.css +++ b/packages/docs/.vitepress/theme/styles/index.css @@ -1,3 +1,5 @@ +@import './api-flavor.css'; + :root { --vp-c-brand-1: #5591d8; --vp-c-brand-2: #336cb0; diff --git a/packages/docs/advanced/lazy-queries.md b/packages/docs/advanced/lazy-queries.md index 9e59df4e..76a1484a 100644 --- a/packages/docs/advanced/lazy-queries.md +++ b/packages/docs/advanced/lazy-queries.md @@ -2,6 +2,12 @@ [`useLazyQuery`](/api/composable/functions/useLazyQuery) is for queries where the variables are not known up front. Reach for it when a query should run in response to a user action (search submit, button click, modal open) rather than automatically on mount. +:::: components-api +::: warning Composition API only +There is no ``. +::: +:::: + ## When to use lazy vs enabled | Situation | Tool | @@ -14,14 +20,8 @@ ## Basic usage -```vue twoslash +```vue ``` -Without `clientId`, the default client handles the operation. The same option works on [`useMutation`](/data/mutations), [`useSubscription`](/data/subscriptions), [`useLazyQuery`](/advanced/lazy-queries), and [`useFragment`](/data/fragments). +The same option works on [`useMutation`](/data/mutations), [`useSubscription`](/data/subscriptions), [`useLazyQuery`](/advanced/lazy-queries), and [`useFragment`](/data/fragments). +:::: + +:::: components-api +Set the `clientId` prop: + +```vue + + + +``` + +[``](/api/components/ApolloMutation), +[``](/api/components/ApolloSubscription) and +[``](/api/components/ApolloFragment) take the same prop. +[``](/api/components/ApolloSubscribeToMore) always uses whichever +client its enclosing `` picked. + +Because the prop is a normal binding, a client can be chosen per element without any of the +plumbing a composable would need: + +```vue-html + +``` +:::: ## Resolving clients imperatively @@ -85,24 +113,32 @@ await analytics.mutate({ `resolveClient()` with no argument returns the default client. +:::: components-api +This is for code, not templates. An element that already picks its client with `clientId` +never needs to resolve one by hand, so reach for `useApolloClient` only where the call +happens outside the template: a store action, a route guard, an event handler that talks to +the client directly. +:::: + ## Switching clients reactively `clientId` is read each time the underlying observable is created. You can vary it based on a ref to switch clients at runtime, but the query is re-created whenever it changes, which means the previous result is lost and a new fetch starts. -```ts twoslash -import { TypedDocumentNode } from '@apollo/client' -import { useQuery } from '@vue/apollo-composable' -import { ref } from 'vue' - -declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode -const QUERY: TypedDocumentNode = gql`` -// ---cut--- +:::: composition-api +```ts const env = ref<'default' | 'staging'>('default') const { current } = useQuery(QUERY, () => ({ clientId: env.value, })) ``` +:::: + +:::: components-api +```vue-html + +``` +:::: This is fine for occasional switches (an admin tool toggling between staging and prod). For per-feature splits, prefer setting `clientId` once per component or per query, not reactively. @@ -126,6 +162,14 @@ const result = cleanup(() => { See [Outside Components](/advanced/outside-components) for the full pattern. +:::: components-api +::: warning Composition API only +`provideApolloClients` exists to give composables an injection context they would otherwise +lack. Components run inside a template, so they always have one and resolve `clientId` +against the `ApolloClients` map the app provided. +::: +:::: + ## When to reach for multiple clients | Situation | Pattern | diff --git a/packages/docs/advanced/outside-components.md b/packages/docs/advanced/outside-components.md index 797730b6..33c96b54 100644 --- a/packages/docs/advanced/outside-components.md +++ b/packages/docs/advanced/outside-components.md @@ -2,7 +2,18 @@ Vue Apollo's composables rely on Vue's injection system to find the Apollo client. Inside a component's ` + + +``` + +`#loading` covers only the gap before the first chunk. From there `#data` stays mounted +for the rest of the stream. + +Listen for `@streamingResult` if you want each chunk imperatively, and use the default +slot when you need `resultState` itself to tell `'streaming'` from `'complete'`. +:::: The `resultState` lifecycle for a `@defer` query: @@ -116,6 +171,7 @@ The `resultState` lifecycle for a `@defer` query: Because deferred fields may be absent during `streaming`, their TypeScript types should reflect that. With GraphQL Codegen, this happens automatically through the `Incremental` / `Streaming` type helpers. With manual `TypedDocumentNode`, mark deferred fields as optional in your `TData` type. +:::: composition-api Inside your component: ```ts @@ -133,6 +189,36 @@ if (current.value.resultState === 'complete') { console.log(current.value.result.user.activity) } ``` +:::: + +:::: components-api +The default slot hands you the same discriminated union, so `resultState` narrows `result` +in the template exactly as it does in script. See +[Two ways to read the result](/data/queries#two-ways-to-read-the-result) for how the two +rendering modes differ: + +```vue + + + +``` +:::: ## Combining with Suspense @@ -154,6 +240,18 @@ const { current } = await useQuery(PROFILE_QUERY, { Use this carefully: it negates the latency benefit of `@defer`. Most of the time, you want the default (render fast, fill in slow) and only opt into `awaitComplete` for screens where partial data would be misleading. +:::: components-api +`awaitComplete` reaches the component through `options`, and it still governs SSR prefetch +there, so the server waits for the whole response before rendering: + +```vue-html + +``` + +On the client it has nothing to act on, because `` cannot suspend. The slots +render as chunks land either way. See [Suspense](/data/suspense) for why. +:::: + ## Refetching a streaming query Refetching a `@defer` query goes through the streaming lifecycle again. The previous `result` stays available while the new response streams in. `networkStatus` reports `refetch` initially, then `streaming`, then `ready`. @@ -170,6 +268,7 @@ query Alphabet { } ``` +:::: composition-api The server can send letters one by one. `current.result.letters` grows as items arrive: ```ts @@ -179,6 +278,11 @@ watch(() => current.value.result?.letters, (letters) => { console.log(letters) // ['a'], ['a', 'b'], ['a', 'b', 'c'], ... }) ``` +:::: + +:::: components-api +The list grows in place, so a `v-for` inside `#data` simply re-renders as each chunk lands. +:::: `@stream` shares the same `resultState` transitions as `@defer`. diff --git a/packages/docs/api/components/ApolloFragment.md b/packages/docs/api/components/ApolloFragment.md new file mode 100644 index 00000000..332372f3 --- /dev/null +++ b/packages/docs/api/components/ApolloFragment.md @@ -0,0 +1,58 @@ +# ApolloFragment + + + +## Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `fragment` *(required)* | `DocumentNode \| TypedDocumentNode` |   | A GraphQL fragment document. | +| `from` *(required)* | `NonNullable>` |   | Cache identifiable entity to read the fragment from.

Accepts a masked object from a parent query, a `{ __ref }` reference, or a cache ID string. | +| `fragmentName` | `string \| undefined` |   | Name of the fragment to use, when the document declares more than one. | +| `variables` | `TVariables \| undefined` |   | Variables used when reading the fragment. | +| `optimistic` | `boolean \| undefined` |   | Read from optimistic cache data. | +| `clientId` | `string \| undefined` |   | Name of the client to use, from the provided `ApolloClients` map. | +| `options` | `useFragment.Options \| undefined` |   | Escape hatch for any other `useFragment` option. | + +## Events + +| Name | Payload | +| --- | --- | +| `nextState` | `[state: RenameKey, "dataState", "resultState">, "data", "result">]` | + +## Slots + +### `#data` + +Opinionated mode. Providing this slot selects it, and `data` has every field. + +| Slot prop | Type | +| --- | --- | +| `data` | `TData` | + +### `#incomplete` + +Shown while fields are still missing from the cache. Opinionated mode only. + +| Slot prop | Type | +| --- | --- | +| `data` | `DeepPartial` | +| `missing` | `MissingTree \| undefined` | + +### `#default` + +Raw mode. Receives the flattened fragment state. + +Rendered in both modes, so nested children still mount. + +Receives `RenameKey, "dataState", "resultState">, "data", "result">`. + +## Exposed + +A template ref receives the full [`useFragment.Result`](/api/composable/@vue/namespaces/useFragment/interfaces/Result) surface, with refs unwrapped. + +```vue + +``` diff --git a/packages/docs/api/components/ApolloMutation.md b/packages/docs/api/components/ApolloMutation.md new file mode 100644 index 00000000..737f2d7c --- /dev/null +++ b/packages/docs/api/components/ApolloMutation.md @@ -0,0 +1,42 @@ +# ApolloMutation + + + +## Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `mutation` *(required)* | `TypedDocumentNode` |   | The mutation document. | +| `variables` | `TVariables \| undefined` |   | Variables applied to every call, unless overridden in `mutate()`. | +| `clientId` | `string \| undefined` |   | Name of the client to use, from the provided `ApolloClients` map. | +| `options` | `useMutation.Options \| undefined` |   | Escape hatch for any other `useMutation` option. | + +## Events + +| Name | Payload | +| --- | --- | +| `error` | `[error: ErrorLike]` | +| `done` | `[result: ApolloClient.MutateResult>]` | + +## Slots + +### `#default` + +| Slot prop | Type | +| --- | --- | +| `mutate` | `useMutation.MutateFunction` | +| `loading` | `boolean` | +| `error` | `ErrorLike \| undefined` | +| `called` | `boolean` | +| `result` | `TData \| null \| undefined` | +| `reset` | `() => void` | + +## Exposed + +A template ref receives the full [`useMutation.Result`](/api/composable/@vue/namespaces/useMutation/interfaces/Result) surface, with refs unwrapped. + +```vue + +``` diff --git a/packages/docs/api/components/ApolloQuery.md b/packages/docs/api/components/ApolloQuery.md new file mode 100644 index 00000000..a0848b89 --- /dev/null +++ b/packages/docs/api/components/ApolloQuery.md @@ -0,0 +1,88 @@ +# ApolloQuery + + + +## Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `query` *(required)* | `TypedDocumentNode` |   | The query document. | +| `variables` | `TVariables \| undefined` |   | Variables for the query. | +| `disabled` | `boolean \| undefined` | `false` | Skip execution until a condition is met. | +| `fetchPolicy` | `useQuery.Options["fetchPolicy"] \| undefined` |   | How the query reads from and writes to the cache. | +| `pollInterval` | `number \| undefined` |   | Refetch every N milliseconds. | +| `debounce` | `number \| undefined` |   | Delay variable updates (ms). `loading` covers the window. | +| `throttle` | `number \| undefined` |   | Throttle variable updates (ms). `loading` covers the window. | +| `keepPreviousResult` | `boolean \| undefined` |   | Keep the previous result on screen while new variables are in flight.

Off by default, as in `useQuery`. Opt in for search and pagination, where clearing the list on every change flickers. | +| `clientId` | `string \| undefined` |   | Name of the client to use, from the provided `ApolloClients` map. | +| `empty` | `((data: TData) => boolean) \| undefined` |   | Decides whether a result counts as empty, selecting the `#empty` slot.

Without it `#empty` is never used, so `#data` handles the empty case itself. | +| `options` | `useQuery.Options \| undefined` |   | Escape hatch for any other `useQuery` option. | + +## Events + +| Name | Payload | +| --- | --- | +| `result` | `[data: TData]` | +| `completeResult` | `[data: TData]` | +| `partialResult` | `[data: TData]` | +| `streamingResult` | `[data: TData]` | +| `error` | `[error: ErrorLike]` | +| `nextState` | `[state: useQuery.Current]` | + +## Slots + +### `#data` + +Opinionated mode. Providing this slot selects it, and `data` is never undefined. + +| Slot prop | Type | +| --- | --- | +| `data` | `TData` | +| `error` | `ErrorLike \| undefined` | +| `isPreviousResult` | `boolean` | +| `loading` | `boolean` | +| `pending` | `boolean` | +| `refetch` | `(variables?: TVariables \| undefined) => Promise, "data", "result"> \| undefined>` | +| `fetchMore` | `(options: ObservableQuery.FetchMoreOptions) => Promise, "data", "result">> \| undefined` | + +### `#loading` + +Shown while the first result is loading. Opinionated mode only. + +No slot props. + +### `#error` + +Shown when the query failed and there is nothing to display. Opinionated mode only. + +| Slot prop | Type | +| --- | --- | +| `error` | `ErrorLike` | +| `refetch` | `(variables?: TVariables \| undefined) => Promise, "data", "result"> \| undefined>` | + +### `#empty` + +Shown when the `empty` prop returns `true`, and as the last branch when the query +settles with nothing to show. Without the `empty` prop, `#data` owns the empty result. + +No slot props. + +### `#default` + +Raw mode. Receives the flattened query state and its methods. + +Rendered in both modes, so nested renderless children still mount. + +Receives `useQuery.Current> & Pick`. + +Every field of [`useQuery.Current`](/api/composable/@vue/namespaces/useQuery/interfaces/Current) is present, so `resultState` narrows `result` here exactly as it does in script. + +## Exposed + +A template ref receives the full [`useQuery.Result`](/api/composable/@vue/namespaces/useQuery/interfaces/Result) surface, with refs unwrapped. + +```vue + +``` diff --git a/packages/docs/api/components/ApolloSubscribeToMore.md b/packages/docs/api/components/ApolloSubscribeToMore.md new file mode 100644 index 00000000..d51f38a4 --- /dev/null +++ b/packages/docs/api/components/ApolloSubscribeToMore.md @@ -0,0 +1,18 @@ +# ApolloSubscribeToMore + + + +## Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `document` *(required)* | `TypedDocumentNode` |   | The subscription document. | +| `variables` | `TVariables \| undefined` |   | Variables for the subscription. | +| `updateQuery` | `SubscribeToMoreUpdateQueryFn \| undefined` |   | Merges incoming subscription data into the parent query's result. | +| `context` | `DefaultContext \| undefined` |   | Context passed to the link chain for this subscription. | + +## Events + +| Name | Payload | +| --- | --- | +| `error` | `[error: ErrorLike]` | diff --git a/packages/docs/api/components/ApolloSubscription.md b/packages/docs/api/components/ApolloSubscription.md new file mode 100644 index 00000000..5a8cc961 --- /dev/null +++ b/packages/docs/api/components/ApolloSubscription.md @@ -0,0 +1,44 @@ +# ApolloSubscription + + + +## Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `subscription` *(required)* | `TypedDocumentNode` |   | The subscription document. | +| `variables` | `TVariables \| undefined` |   | Variables for the subscription. | +| `disabled` | `boolean \| undefined` | `false` | Stop the subscription. | +| `clientId` | `string \| undefined` |   | Name of the client to use, from the provided `ApolloClients` map. | +| `options` | `useSubscription.Options \| undefined` |   | Escape hatch for any other `useSubscription` option. | + +## Events + +| Name | Payload | +| --- | --- | +| `result` | `[data: TData]` | +| `error` | `[error: ErrorLike]` | +| `complete` | `[]` | + +## Slots + +### `#default` + +| Slot prop | Type | +| --- | --- | +| `result` | `TData \| undefined` | +| `loading` | `boolean` | +| `error` | `ErrorLike \| undefined` | +| `start` | `() => void` | +| `stop` | `() => void` | +| `restart` | `() => Promise` | + +## Exposed + +A template ref receives the full [`useSubscription.Result`](/api/composable/@vue/namespaces/useSubscription/interfaces/Result) surface, with refs unwrapped. + +```vue + +``` diff --git a/packages/docs/api/components/index.md b/packages/docs/api/components/index.md new file mode 100644 index 00000000..452ff5e3 --- /dev/null +++ b/packages/docs/api/components/index.md @@ -0,0 +1,9 @@ +# Components API + + + +- [ApolloQuery](./ApolloQuery) +- [ApolloMutation](./ApolloMutation) +- [ApolloSubscription](./ApolloSubscription) +- [ApolloSubscribeToMore](./ApolloSubscribeToMore) +- [ApolloFragment](./ApolloFragment) diff --git a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md index d795580f..4f9dd982 100644 --- a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md +++ b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md @@ -165,9 +165,9 @@ Debounce variable updates (ms). Keep previous result while loading new data. -The retained result is reported as a normal result — `resultState`, `result` and -`partial` all describe it — with `isPreviousResult` set to `true` so it can be -told apart from a fresh one. +The retained result is reported as a normal result, so `resultState`, `result` and +`partial` all describe it, with `isPreviousResult` set to `true` so it can be told +apart from a fresh one. #### Default Value diff --git a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md index 388c613a..cbf4db28 100644 --- a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md +++ b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md @@ -430,6 +430,8 @@ Event triggered when query result data is received. Fires when `resultState` is `'complete'`, `'partial'`, or `'streaming'`. Does not fire for `'empty'` state or errors. +A result served from the cache during the `useQuery()` call is replayed on the next tick, so a handler registered right after the call still receives it. + *** ### onStreamingResult diff --git a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md index 47ec5710..f890b7a2 100644 --- a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md +++ b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md @@ -173,9 +173,9 @@ Reactive flag to enable/disable the query. Keep previous result while loading new data. -The retained result is reported as a normal result — `resultState`, `result` and -`partial` all describe it — with `isPreviousResult` set to `true` so it can be -told apart from a fresh one. +The retained result is reported as a normal result, so `resultState`, `result` and +`partial` all describe it, with `isPreviousResult` set to `true` so it can be told +apart from a fresh one. #### Default Value diff --git a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md index 717900e7..92ae1d97 100644 --- a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md +++ b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md @@ -398,6 +398,8 @@ Event triggered when query result data is received. Fires when `resultState` is `'complete'`, `'partial'`, or `'streaming'`. Does not fire for `'empty'` state or errors. +A result served from the cache during the `useQuery()` call is replayed on the next tick, so a handler registered right after the call still receives it. + *** ### onStreamingResult diff --git a/packages/docs/api/index.md b/packages/docs/api/index.md new file mode 100644 index 00000000..44ef92dd --- /dev/null +++ b/packages/docs/api/index.md @@ -0,0 +1,97 @@ +# API Reference + +Vue Apollo ships two public packages. They are two ways to spell the same thing: the +components are built on the composables and expose the same Apollo Client behaviour, so +mixing them in one app, or in one component, is expected. + +| Package | Style | Reference | +| --- | --- | --- | +| [`@vue/apollo-composable`](./composable/) | ` + + +``` +:::: + +:::: components-api +```vue + + + +``` +:::: + +## How the two references are generated + +- **Composable**: [TypeDoc](https://typedoc.org/) over the published declaration files. +- **Components**: [`vue-component-meta`](https://github.com/vuejs/language-tools), which + attaches JSDoc to props, events and slot props. + +Both are regenerated by `pnpm api:generate` and are not edited by hand. + +## Types + +Every option and result type is namespaced under the function or component it belongs to: +`useQuery.Options`, `useMutation.Result`, `useFragment.Current`. The namespaces are +listed under [`@vue/apollo-composable`](./composable/), and the components' props link +into them, since a component prop is almost always the matching composable option. + +For writing typed documents, see [TypeScript](/data/typescript). diff --git a/packages/docs/caching/cache-updates.md b/packages/docs/caching/cache-updates.md index f7ff6695..6011547d 100644 --- a/packages/docs/caching/cache-updates.md +++ b/packages/docs/caching/cache-updates.md @@ -2,16 +2,32 @@ When a mutation modifies data on the server, the local cache needs to reflect the change so the UI updates. Apollo Client handles many cases automatically, but not all. This page covers the patterns for keeping the cache in sync after a mutation. +:::: components-api +::: tip Reading this page with `` +Cache updating is configured entirely through mutation options, and `` +takes those through its `options` prop, which accepts the full +[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options) +object: + +```vue-html + +``` + +So wherever a snippet below reads `useMutation(CREATE_TODO, { ... })`, the `{ ... }` is what +goes into `:options`. Callbacks like `update` are ordinary functions declared in +` + + +``` + +```vue [TodoDetail.vue] + + + +``` + +::: + +Both documents in `./queries` select `id` alongside the fields they render. Without the key +field in the selection set the cache cannot file the two results under the same `Todo:5` +entry, and the components stop tracking each other. +:::: + +After the toggle runs, the mutation result `{ id, completed }` updates `Todo:5` in the cache. Both queries see the change immediately. No `refetchQueries`, no manual subscription. ## When the cache is not enough @@ -112,7 +167,7 @@ For each of those, you describe the cache change yourself or refetch the affecte ## Fetch policies -`fetchPolicy` on `useQuery` controls how the cache is consulted on each call: +`fetchPolicy` controls how the cache is consulted on each call: | Policy | Behavior | |--------|----------| @@ -124,6 +179,14 @@ For each of those, you describe the cache change yourself or refetch the affecte `cache-and-network` is a useful default for screens where freshness matters but you do not want to block on the network: users see the cached result immediately, then the screen updates when the request returns. +:::: components-api +`` takes the policy as a prop: + +```vue-html + +``` +:::: + ## Configuration Apollo Client's cache is configurable. You can tell it how to identify entities, how to merge nested fields, and how to handle pagination. The configuration lives where you create your `InMemoryCache`: diff --git a/packages/docs/data/data-masking.md b/packages/docs/data/data-masking.md index dfa4dadc..2c0cdca9 100644 --- a/packages/docs/data/data-masking.md +++ b/packages/docs/data/data-masking.md @@ -80,6 +80,7 @@ With masking on, fields defined only in a fragment are hidden from queries that ## Reading masked data +:::: composition-api Use [`useFragment`](/api/composable/functions/useFragment) inside the component that owns the fragment: ```vue twoslash @@ -114,6 +115,49 @@ const { current } = useFragment({ ``` `current.result` only contains the fields defined in the fragment. Parent queries do not leak into it, and sibling fragments do not leak either. +:::: + +:::: components-api +Use [``](/api/components/ApolloFragment) inside the component that owns the +fragment. The masked object the parent passed down goes straight into `from`: + +```vue twoslash + + + +``` + +`data` only contains the fields defined in the fragment. Parent queries do not leak into it, +and sibling fragments do not leak either. + +With masking enabled, a component cannot read the fragment's fields from the `post` prop +it receives. `` unmasks them in the template. +:::: ## Fixing the parent component @@ -178,7 +222,12 @@ This helps you find every implicit dependency before removing the `@unmask` dire | `subscribeToMore` `updateQuery` callback | No | | Cache APIs (`readQuery`, `readFragment`) | No | -Cache APIs and mutation update callbacks deal with the underlying cache directly, so masking does not apply to them. +:::: components-api +Every row above applies to the components too. A masked field is missing from +[``](/api/components/ApolloQuery)'s `#data` slot and from its `@result` +payload, and still present inside an `update` callback passed through +[``](/api/components/ApolloMutation)'s `options` prop. +:::: ## Incremental adoption @@ -216,7 +265,7 @@ If you use TypeScript with GraphQL Codegen: ### 4. Refactor components 1. Watch the console for "accessing masked field" warnings. -2. Update components to use `useFragment` for the data they own. +2. Read the data a component owns through [`useFragment`](/api/composable/functions/useFragment) or [``](/api/components/ApolloFragment). 3. Add any required fields to parent queries explicitly. 4. Remove `@unmask` directives when no warnings remain. diff --git a/packages/docs/data/error-handling.md b/packages/docs/data/error-handling.md index 5c02810d..db6b6fee 100644 --- a/packages/docs/data/error-handling.md +++ b/packages/docs/data/error-handling.md @@ -57,6 +57,7 @@ Network errors are represented by several classes: ### Queries +:::: composition-api Read errors through `current.error`: ```vue twoslash @@ -85,9 +86,50 @@ const { current } = useQuery(gql` ``` +:::: + +:::: components-api +The `#error` slot covers the case where the query failed and there is nothing to show. It +receives `refetch`, so the retry lives in the template: + +```vue twoslash + + + +``` + +Slot resolution is data-first, so a refetch that fails over data already on screen keeps +rendering `#data` rather than replacing a working page with an error. Those failures reach +the `@error` event, where a toast is usually the right treatment. See +[Two ways to read the result](/data/queries#two-ways-to-read-the-result). +:::: ### Mutations +:::: composition-api Read errors through the `error` ref: ```vue twoslash @@ -110,9 +152,42 @@ const { mutate, error, loading } = useMutation(gql`

``` +:::: + +:::: components-api +`error` is a slot prop on the default slot: + +```vue + + + +``` + +`` bridges `@error` to `onError` only while the parent is actually +listening, so binding the event is what makes `mutate()` resolve here. Leave `@error` +unbound and the same call rejects instead, exactly as it would under `useMutation`'s +`throws: 'auto'`. See [Error throwing behavior](/data/mutations#error-throwing-behavior). +:::: ### Subscriptions +:::: composition-api Read errors through the `error` ref, or register an `onError` callback: ```vue twoslash @@ -139,9 +214,39 @@ onError((err) => { ``` +:::: + +:::: components-api +Read errors from the `error` slot prop, next to the `restart` that reconnects: + +```vue twoslash + + + +``` + +The same failure is also emitted as `@error`, which is what +[Error event hooks](#error-event-hooks) below covers. +:::: ## Error event hooks +:::: composition-api Every composable exposes an `onError` event hook for imperative handling: ```ts twoslash @@ -163,11 +268,48 @@ onError((error) => { // errorTracker.capture(error) }) ``` +:::: + +:::: components-api +``, ``, `` and `` +emit `@error`. `` has none. + +```vue + + + +``` + +Unlike `#error`, the event fires for every failure, including one that lands on top of a +result the reader is still looking at. That makes it the right place for logging and +toasts, and `#error` the right place for the empty-page treatment. +:::: ## Mutation throwing behavior By default, `mutate()` throws when no `onError` handler is registered. The `throws` option controls this: +:::: composition-api ```ts twoslash import { TypedDocumentNode } from '@apollo/client' import { useMutation } from '@vue/apollo-composable' @@ -208,6 +350,7 @@ async function handleSubmit() { } } ``` +:::: | Value | Behavior | |-------|----------| @@ -215,10 +358,25 @@ async function handleSubmit() { | `'always'` | Always throws | | `'never'` | Never throws | +:::: components-api +`` has the same default. Binding `@error` is what registers a handler under +`'auto'`, so `mutate()` resolves and the failure arrives on the event; with nothing bound it +rejects. `throws` has no dedicated prop, so set it through `options`, which takes the full +[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options) +object: + +```vue-html + +``` + +See [Error throwing behavior](/data/mutations#error-throwing-behavior) for the full table. +:::: + ## Error policies By default, Apollo Client discards partial data when a GraphQL error occurs and populates `error`. Change this with `errorPolicy`: +:::: composition-api ```ts twoslash import { TypedDocumentNode } from '@apollo/client' import { useQuery } from '@vue/apollo-composable' @@ -230,6 +388,16 @@ const { current } = useQuery(GET_USERS, { errorPolicy: 'all', }) ``` +:::: + +:::: components-api +`errorPolicy` has no dedicated prop; set it through `options`, which takes the full +[`useQuery.Options`](/api/composable/@vue/namespaces/useQuery/interfaces/Options) object: + +```vue-html + +``` +:::: | Policy | Behavior | |--------|----------| @@ -241,6 +409,7 @@ const { current } = useQuery(GET_USERS, { With `errorPolicy: 'all'`, partial data is available alongside the error: +:::: composition-api ```vue twoslash + + +``` + +The two modes mix, so a `#data` slot can still handle the everything-worked case while the +default slot renders the warning banner above it. +:::: ## Identifying error types @@ -359,8 +563,8 @@ const errorLink = new ErrorLink(({ error, operation, forward }) => { }) ``` -::: warning -If the retried operation also fails, those errors do not reach `ErrorLink` again. An `ErrorLink` can only retry a particular operation once. +::: warning `ErrorLink` retries an operation only once +If the retried operation also fails, those errors do not reach `ErrorLink` again. ::: ### Retry on network errors @@ -390,6 +594,7 @@ const link = from([retryLink, new HttpLink({ uri: '/graphql' })]) `reset()` clears mutation errors so the form can be tried again: +:::: composition-api ```vue twoslash + + +``` + +See [Resetting state](/data/mutations#resetting-state) for what else it clears. +:::: ## Next steps diff --git a/packages/docs/data/fragments.md b/packages/docs/data/fragments.md index 5f3571ae..df933db1 100644 --- a/packages/docs/data/fragments.md +++ b/packages/docs/data/fragments.md @@ -30,14 +30,24 @@ const GET_USER = gql` ` ``` -## Reading fragment data with `useFragment` +## Reading fragment data +:::: composition-api [`useFragment`](/api/composable/functions/useFragment) creates a reactive binding to fragment data in the Apollo cache. It watches for changes and updates automatically when the cache changes. +:::: + +:::: components-api +[``](/api/components/ApolloFragment) creates a reactive binding to fragment +data in the Apollo cache. It watches for changes and updates automatically when the cache +changes. +:::: ::: warning Cache identification required -`useFragment` only works with entities the cache can identify. Each entity needs a unique cache ID, normally `__typename` plus the entity's key field (usually `id`). +This only works with entities the cache can identify. Each entity needs a unique cache ID, +normally `__typename` plus the entity's key field (usually `id`). ::: +:::: composition-api ```vue twoslash + + +``` + +The `from` prop accepts an object with `__typename` and the key field, a reference object +like `{ __ref: 'User:1' }`, or a string cache ID such as `'User:1'`. + +Like ``, the component has two modes. Providing `#data` selects the +opinionated one, where `data` has every field the fragment asks for. `#incomplete` covers +the case where the cache is missing some of them, and receives both the partial data and a +`missing` tree describing what is absent: + +```vue-html + + + + +``` + +Using only the default slot gives you raw mode, with the flattened +[`useFragment.Current`](/api/composable/@vue/namespaces/useFragment/interfaces/Current) +state to narrow yourself. +:::: ### Working with arrays +:::: composition-api Pass an array of entities to read several at once: ```ts twoslash @@ -94,9 +171,47 @@ const { current } = useFragment({ ``` `current.result` is an array of items where each index lines up with `from`. `resultState` is `'complete'` only when every item is complete. +:::: + +:::: components-api +`` reads a **single** entity. For a list, put it in a `v-for`: + +```vue twoslash + + + +``` + +`useFragment`'s array form reports `resultState` as `'complete'` only when *every* entity +is complete, so one entity missing a field downgrades the whole list. One component per +row gives each entity its own complete/incomplete branch, and `#data` stays typed as a +single entity instead of an array. +:::: ### Event hook +:::: composition-api React to fragment data changes imperatively: ```ts twoslash @@ -116,6 +231,19 @@ onNextState((state) => { console.log('Fragment data changed:', state) }) ``` +:::: + +:::: components-api +`onNextState` is emitted as `@nextState`: + +```vue-html + +``` +:::: ## Colocating fragments @@ -129,6 +257,7 @@ Colocate fragment definitions with the components that read them. Each component Since `export const` is not allowed in ` + + +``` + +```vue [UserProfile.vue] + + + + + +``` + +::: +:::: ::: tip Fragment naming Prefix fragment names with the component name (`UserAvatarFields`) so they remain identifiable when many fragments are composed together. @@ -217,10 +404,20 @@ const GET_USER = gql` ## Options and result reference +:::: composition-api For every available option and method, see: - [`useFragment.Options`](/api/composable/@vue/namespaces/useFragment/interfaces/Options) - [`useFragment.Result`](/api/composable/@vue/namespaces/useFragment/interfaces/Result) +:::: + +:::: components-api +For every prop, event and slot prop, see: + +- [``](/api/components/ApolloFragment) +- [`useFragment.Options`](/api/composable/@vue/namespaces/useFragment/interfaces/Options), for the `options` prop +- [`useFragment.Result`](/api/composable/@vue/namespaces/useFragment/interfaces/Result), for what a template ref exposes +:::: ## Next steps diff --git a/packages/docs/data/mutations.md b/packages/docs/data/mutations.md index acdbf92a..77dc054d 100644 --- a/packages/docs/data/mutations.md +++ b/packages/docs/data/mutations.md @@ -1,9 +1,12 @@ # Mutations -This page covers updating data with the [`useMutation`](/api/composable/functions/useMutation) composable. +A GraphQL mutation is a write request. Like a [query](/data/queries) it names the fields it wants back, so the server returns the updated object in the same round trip that changed it. + +That return value is what makes mutations more than a POST. Apollo Client writes it into the same normalized cache your queries read from, so a mutation that returns the entity it modified updates every query already showing it, with no refetch and no manual invalidation. ## Executing a mutation +:::: composition-api Unlike [`useQuery`](/data/queries), `useMutation` does not execute automatically. It returns a `mutate` function that you call when you want the mutation to run: ```vue twoslash @@ -52,13 +55,67 @@ async function handleSubmit() { - `called` is `true` once `mutate` has been called at least once. - `result` holds the most recent mutation result data. - `reset()` resets `result`, `error`, `loading`, and `called` to their initial state. +:::: -::: tip Why flat refs and not `current`? -`useMutation` does not expose a `current` discriminated union the way `useQuery` does. A mutation is request-response: it does not have streaming or partial states. Using individual refs keeps the API close to how you naturally consume a mutation. See [TypeScript](/data/typescript#composable-return-value-shapes) for the comparison. -::: +:::: components-api +`` is renderless: it has one slot, and nothing happens until you call +`mutate` from it. + +```vue twoslash + + + +``` + +Nothing is listening for `@error` here, so `mutate()` rejects on failure: the chain needs a +`.catch`, and the input is cleared only when the mutation actually succeeded. Bind `@error` +instead and `mutate()` resolves, with the failure arriving on the event. See +[Error throwing behavior](#error-throwing-behavior). + +The slot gives you the same surface `useMutation` returns, with the refs unwrapped: + +- `mutate(options?)` triggers the mutation and returns a promise resolving to the result. +- `loading` is `true` while the mutation is in flight. +- `error` contains any error from the mutation. +- `called` is `true` once `mutate` has been called at least once. +- `result` holds the most recent mutation result data. +- `reset()` clears `result`, `error` and `called`. +:::: ## Variables +:::: composition-api The most common pattern is to pass variables when calling `mutate`: ```ts twoslash @@ -94,9 +151,27 @@ mutate() ``` When variables come from both composable options and the `mutate` call, the call-time variables win, merged on top of the composable variables. +:::: + +:::: components-api +Pass variables when calling `mutate` from the slot: + +```vue-html + + + + + +``` + +`throws` has no dedicated prop; set it through `options`: + +```vue-html + +``` + +| Value | Behavior | +|-------|----------| +| `'auto'` | Throws unless `@error` is bound, or a handler was registered via a template ref **(default)** | +| `'always'` | Always throws | +| `'never'` | Never throws; read the `error` slot prop | + +::: tip +The component bridges `@error` to `onError` only while the parent is actually listening, so +`throws: 'auto'` means exactly what it means in `useMutation`. It re-checks between renders, +so a listener bound conditionally is picked up when it appears. +::: +:::: ## Multiple calls in flight diff --git a/packages/docs/data/queries.md b/packages/docs/data/queries.md index b4a62c71..bedf1402 100644 --- a/packages/docs/data/queries.md +++ b/packages/docs/data/queries.md @@ -1,9 +1,12 @@ # Queries -This page shows how to fetch GraphQL data in Vue with the [`useQuery`](/api/composable/functions/useQuery) composable. +A GraphQL query is a read request that names exactly the fields you want back. The server answers with a response shaped like the query, so a single round trip fetches what a screen needs and nothing more. Writes go through [mutations](/data/mutations) instead, and live updates through [subscriptions](/data/subscriptions). + +Apollo Client does more than send it. Each result is normalized into a cache keyed by entity, so two components asking for the same object share one copy and one request, and a later write to that object updates both. ## Executing a query +:::: composition-api Call `useQuery` inside ` + + +``` + +Inside `#data`, `data` is the query's result type and is never `undefined`. It is typed as +the complete result even when the state is `partial` or `streaming`, though, so when the +difference matters, read the query through the default slot and narrow on `resultState` +instead. See [Using partial data](/data/error-handling#using-partial-data). + +The component decides which of the four slots to render. + +`#empty` is the one that needs telling what empty means: the `empty` prop is a predicate +over the result, and without it a result is never treated as empty. Leave both out and +`#data` handles the empty case itself. + +`#empty` is also the last branch in the chain, so a query that settles with no result, no +error and nothing in flight renders it rather than nothing at all. A `disabled` query is +the exception: it renders nothing. +:::: -### Why we recommend `current` +## Two ways to read the result + +:::: components-api +`` renders in one of two modes, chosen by which slots you provide. + +**Opinionated mode**, selected by providing `#data`, is the example above. The component +picks a slot for you: + +| Slot | Rendered when | +| --- | --- | +| `#empty` | The `empty` prop returns `true` for the current result | +| `#data` | There is a result to show | +| `#error` | The query failed and there is nothing to show | +| `#loading` | The first request is still in flight | + +The order is significant, and it is **data-first**: once there is a result, `#data` keeps +rendering through a background refetch and through a *failed* refetch. Showing an error +page over data the user can still read is almost never what you want, so `#error` is +reserved for the case where there is genuinely nothing else to display. A refetch that +fails reaches `#data` through its own `error` slot prop; see +[Error handling](#error-handling). + +If you leave a slot out, that branch renders nothing. + +**Raw mode**, selected by using only the default slot, hands you the same flattened state +`useQuery` exposes and gets out of the way: + +```vue twoslash + + + +``` +The default slot receives every field of [`useQuery.Current`](/api/composable/@vue/namespaces/useQuery/interfaces/Current) +plus `refetch`, `fetchMore`, `updateQuery`, `subscribeToMore`, `start`, `stop` and +`restart`. `resultState` narrows `result` exactly as it does in script. + +::: tip The default slot always renders +The default slot is rendered in **both** modes, not as a `v-else` to `#data`. That is what +lets renderless children like [``](/data/subscriptions#subscribing-to-query-updates) +sit inside an `` that also uses `#data`. +::: +:::: + +:::: composition-api `useQuery` exposes the same information in two shapes: a single `current` ref containing the discriminated union, and individual refs like `result`, `loading`, `error` that you can destructure separately. We recommend `current` for any code that reads `result`. The `resultState` field narrows the type of `result`: @@ -67,9 +191,11 @@ if (current.value.resultState === 'complete') { The individual refs work, but `result.value` is always typed as `TData | undefined` without context. Reading `current` keeps loading, network status, and data narrowing in one place. The standalone refs (`result`, `loading`, `error`, `networkStatus`) remain available for cases where you only care about one of them and do not need to access `result`. +:::: ## Variables +:::: composition-api Pass variables in the options object: ```vue twoslash @@ -103,9 +229,8 @@ When `breed.value` changes, the query re-executes with the new value. 2. **A ref or getter that returns the whole variables object.** Useful when you compute the variables from other reactive state. 3. **A plain object.** Static variables, no reactivity. -### Variables as a getter - -For props or values that change over time, a getter avoids losing reactivity: +**Reading a prop?** Use a getter. A destructured prop is read once, so passing it directly +would freeze the variable at its initial value: ```vue twoslash ``` +:::: -### Throttle and debounce +:::: components-api +Bind variables like any other prop: + +```vue twoslash + + + +``` -For inputs that update rapidly (search boxes, sliders), `throttle` and `debounce` reduce how often the query re-executes: +When `breed` changes the query re-executes with the new value. You do not need refs or +getters inside the object the way `useQuery` allows. +:::: +## Throttle and debounce + +For inputs that update rapidly, such as search boxes, sliders and filter panels, `throttle` and `debounce` reduce how often the query re-executes. Use `debounce` to wait for keystrokes to settle, `throttle` to cap the rate. They are mutually exclusive. + +:::: composition-api ```ts twoslash import { TypedDocumentNode } from '@apollo/client' import { useQuery } from '@vue/apollo-composable' import { ref } from 'vue' -declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ search: { id: string }[] }, { q: string }> -const SEARCH = gql`` +declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ products: { id: string, name: string }[] }, { term: string }> +const SearchProducts = gql`` // ---cut--- const term = ref('') -const { current } = useQuery(SEARCH, { - variables: { q: term }, +const { current } = useQuery(SearchProducts, { + variables: { term }, debounce: 300, // wait 300ms after the last change before re-executing }) ``` +:::: + +:::: components-api +```vue twoslash + + + +``` +:::: + +### What `loading` and `pending` mean here + +Delaying a request splits "the user changed something" from "a request is in flight", and the two are reported separately: + +| Field | `true` when | +| --- | --- | +| `loading` | From the moment new variables are accepted until the results land, **including** the window where the timer has not elapsed | +| `pending` | Only while the timer is running, before the request goes out | +| `networkStatus` | Describes the network alone, and stays `ready` throughout the window | + +`loading` covering the delay is the point: a search field shows its spinner on the keystroke rather than 300ms later. + +Reach for `pending` when you need the delay itself: request metrics, or a "cancel" affordance that only makes sense before the request is sent. + +:::: composition-api +```ts twoslash +import { TypedDocumentNode } from '@apollo/client' +import { useQuery } from '@vue/apollo-composable' +import { ref } from 'vue' + +declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ products: { id: string }[] }, { term: string }> +const SearchProducts = gql`` +// ---cut--- +const term = ref('') + +const { current } = useQuery(SearchProducts, { + variables: { term }, + debounce: 300, +}) + +// current.loading is true from the keystroke until the results land +// current.pending is true only while the debounce timer is running +``` +:::: + +:::: components-api +`loading` and `pending` both reach the `#data` slot, alongside the data itself: + +```vue-html + +``` +:::: + +Two cases deliberately never report as pending, because no request will follow: variables rebuilt with deeply equal contents, and variables that change and change back before the timer elapses. + +### Pair it with `keepPreviousResult` + +Debouncing on its own still clears the result the moment new variables are accepted, so the list a user is reading disappears for the length of the delay plus the request. Keeping the previous result closes that gap: + +:::: composition-api +```vue twoslash + + + +``` + +Without it, every keystroke drops `current.result` back to `undefined` and the template falls through to the loading branch. With it, the old results stay on screen and `isPreviousResult` marks them as stale so you can dim them. +:::: + +:::: components-api +Add the `keepPreviousResult` prop: + +```vue twoslash + + + +``` + +Without it, `#loading` takes over on every keystroke. With it, `#loading` is only used for the very first search, and `isPreviousResult` marks the stale list so you can dim it. +:::: + +See [Keeping previous data](#keeping-previous-data) for how a retained result reports itself. ## Caching Apollo Client caches query results in a normalized in-memory cache. When you execute the same query again with the same variables, the result comes back instantly from the cache. +The cache belongs to the client, not to the composable or the component, so this is +identical for both APIs. Two `` elements with the same query and variables +read the same cache entry, and a mutation that updates it refreshes both. + +:::: composition-api ```vue twoslash ``` +:::: + +:::: components-api +```vue twoslash + + + +``` +:::: Read [Caching Overview](/caching/overview) to understand cache behavior in depth. ## Loading states -`current.loading` is `true` while the query is in flight: +:::: composition-api +`current.loading` is `true` while a request is in flight: ```vue twoslash + + +``` + +So the two slots split by whether there is anything on screen, not by whether a request is +running: `#loading` when there is nothing, `#data` with `loading: true` when there is. + +The `pending` slot prop is only ever `true` when the `debounce` or `throttle` prop is set, +since it isolates that delay. With neither, it is always `false`. See +[Throttle and debounce](#throttle-and-debounce). + +For `networkStatus` and the rest of the state, use the default slot or a template ref; see +[Two ways to read the result](#two-ways-to-read-the-result). +:::: For loading indicators that aggregate multiple queries, see [Loading States](/advanced/loading-states). ## Error handling +:::: composition-api `current.error` contains any error that occurred: ```vue twoslash @@ -246,11 +665,87 @@ const { current } = useQuery(QUERY) ``` +:::: + +:::: components-api +The `#error` slot handles the case where the query failed and there is nothing to show. It +receives `refetch`: + +```vue twoslash + + + +``` + +Because slot resolution is data-first, `#error` does **not** render when a query fails while +there is still something on screen: a failed refetch, or a partial result under +`errorPolicy: 'all'`. + +Those failures are not hidden, though. `#data` receives an `error` prop of its own, so you +can keep showing the rows and mark them as stale in the same breath: + +```vue twoslash + + + +``` + +So the two slots divide by whether anything is left to show: `#error` when there is nothing, +`#data` with `error` set when there is. The `@error` event fires either way, which is the +right hook for a toast or for reporting: + +```vue-html + +``` +:::: For comprehensive error handling (error policies, partial data, classifying error types), see [Error Handling](/data/error-handling). ## Fetch policies +:::: composition-api `fetchPolicy` controls how the query interacts with the cache: ```ts twoslash @@ -264,6 +759,15 @@ const { current } = useQuery(QUERY, { fetchPolicy: 'network-only', }) ``` +:::: + +:::: components-api +The `fetchPolicy` prop controls how the query interacts with the cache: + +```vue-html + +``` +:::: | Policy | Description | |--------|-------------| @@ -275,8 +779,26 @@ const { current } = useQuery(QUERY, { `nextFetchPolicy` lets you switch to a different policy after the first request completes. `initialFetchPolicy` resets to a specific policy when variables change. See [`useQuery.Options`](/api/composable/@vue/namespaces/useQuery/interfaces/Options) for details. +:::: components-api +Only the options worth a prop have one. Everything else goes through the `options` prop, +which takes the full [`useQuery.Options`](/api/composable/@vue/namespaces/useQuery/interfaces/Options) +object: + +```vue-html + +``` + +Props and `options` are merged, with props taking precedence, but only where the prop is +actually set. An absent prop leaves the corresponding `options` entry alone rather than +overwriting it with `undefined`, so the two can be combined freely. +:::: + ## Disabling queries +:::: composition-api Set `enabled: false` to skip execution until a condition is met: ```vue twoslash @@ -303,11 +825,51 @@ const { current } = useQuery( ``` While `enabled` is `false`, no observable query exists. When `enabled` flips to `true`, the query starts and behaves like any other `useQuery` from that point on. +:::: + +:::: components-api +Use the `disabled` prop to skip execution until a condition is met: +```vue twoslash + + + +``` + +While `disabled` is `true`, no observable query exists. When it flips to `false`, the query +starts and behaves like any other from that point on. + +::: tip `disabled`, not `enabled` +The prop inverts `useQuery`'s `enabled`. Both spellings work: `options: { enabled }` is left +alone when the `disabled` prop is absent. +::: +:::: + +:::: composition-api For queries that run in response to a user action (search submit, button click), prefer [`useLazyQuery`](/advanced/lazy-queries) instead. +:::: ## Event hooks +:::: composition-api React to query lifecycle events imperatively: ```ts twoslash @@ -336,9 +898,50 @@ onError((error) => { ``` `onResult` has three variants you can listen to individually: `onCompleteResult`, `onPartialResult`, and `onStreamingResult`. The plain `onResult` fires for all three. +:::: + +:::: components-api +Every `useQuery` hook is emitted as a component event: + +```vue + + + +``` + +| Event | Fires when | +| --- | --- | +| `@result` | Any result data arrives: complete, partial or streaming | +| `@completeResult` | A complete result arrives | +| `@partialResult` | A partial result arrives | +| `@streamingResult` | A streaming (`@defer`/`@stream`) chunk arrives | +| `@error` | The query errors, including on a refetch over existing data | +| `@nextState` | Any state change at all: loading, network status, result | + +:::: ## Keeping previous data +:::: composition-api When variables change, `result` is cleared by default while the new request is in flight. Set `keepPreviousResult: true` to keep showing the previous data until the new data arrives: ```ts twoslash @@ -393,32 +996,54 @@ const { current } = useQuery(SearchProducts, { ``` `resultState` describes the data being handed back; `loading`, `networkStatus` and `error` describe the request. Retention only ever changes the former. +:::: -## Loading and the debounce window +:::: components-api +When variables change, the result is cleared by default while the new request is in flight. +For a search field that means `#data` flickering out to `#loading` on every keystroke. Set +`keepPreviousResult` to hold the old list until the new one lands: -`loading` is `true` from the moment the query accepts new variables, not from the moment the request goes out. With `debounce` or `throttle`, that includes the window where the timer has not yet elapsed — so a search-as-you-type field shows a spinner on the keystroke rather than after the delay: - -```ts twoslash +```vue twoslash + -const { loading, pending } = useQuery(SearchProducts, { - variables: { term }, - debounce: 300, -}) -// loading: true from the keystroke until the results land -// pending: true only while the debounce timer is running + ``` -`pending` separates the two halves for the cases that need it (request metrics, cancel affordances). `networkStatus` describes the network alone and stays `ready` throughout the debounce window, so `loading` is deliberately broader than `networkStatus < 7`: it also spans the hand-over where the variables have been accepted but the request has not gone out yet. +`#loading` now only appears for the very first search. Every later one keeps the old list on +screen, marked `isPreviousResult`, until the new one arrives. -Variables that are rebuilt with deeply equal contents never report as pending, since no request will follow. Neither do variables that change and change back before the timer elapses. +A retained result is reported as a normal result: `resultState`, `result` and `partial` all +describe the data you are still showing, so narrowing in the default slot keeps working. +`isPreviousResult` tells the two apart, and `loading` describes the request on its way to +replace it. +:::: ## Awaiting the query @@ -439,12 +1064,31 @@ const { current } = await useQuery(gql` See [Suspense](/data/suspense) for the full pattern, including SSR and streaming considerations. +:::: components-api +::: warning Composition API only +`` cannot suspend. + +Use `await useQuery(...)` in ` + + +``` + +`refetch()` returns a promise that resolves with the new result. `#data` keeps rendering +throughout, with `loading` reporting the request in flight, so a refresh never blanks the +image. +:::: ### Refetch with different variables You can pass new variables for a one-off refetch: +:::: composition-api ```ts twoslash import { TypedDocumentNode } from '@apollo/client' import { useQuery } from '@vue/apollo-composable' @@ -61,16 +103,52 @@ await refetch({ breed: 'poodle' }) // The reactive variables ref does not change console.log(variables.value.breed) // 'bulldog' ``` +:::: + +:::: components-api +```vue twoslash + + + +``` +:::: ::: warning Variables passed to `refetch` are not persisted -`refetch({ id: 2 })` uses those variables for that one request. The reactive `variables` ref keeps its previous value, and the next change to your declared `variables` (or another refetch with no args) goes back to using them. +`refetch({ id: 2 })` uses those variables for that one request. The query's declared variables keep their previous value, and the next change to them, or another refetch with no arguments, goes back to using them. -If you want the new variables to stick, update the reactive variable source instead. +If you want the new variables to stick, change the declared variables instead. ::: ## Polling -Set `pollInterval` (in milliseconds) to re-run a query at a fixed cadence: +:::: composition-api +Set `pollInterval` (in milliseconds) to re-run a query at a fixed interval: ```vue twoslash + + +``` + +`query` is `undefined` while `disabled` is `true`. +:::: ## Refetching after a mutation -A successful mutation often invalidates queries that display the same data. The simplest way to refresh those queries is to list them in `refetchQueries` on `useMutation`: +A successful mutation often invalidates queries that display the same data. The simplest way to refresh those queries is to list them in `refetchQueries`: +:::: composition-api ```ts twoslash import { TypedDocumentNode } from '@apollo/client' import { useMutation } from '@vue/apollo-composable' @@ -156,6 +302,26 @@ const { mutate } = useMutation(CREATE_TODO, { ], }) ``` +:::: + +:::: components-api +```vue-html + + + +``` + +`refetchQueries` accepts documents and operation names alike. Everything on +[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options) is +reachable this way, including `awaitRefetchQueries` and `onQueryUpdated` below. +:::: You can also pass: @@ -171,17 +337,29 @@ An **active query** is one that has at least one subscriber (a mounted component By default, `mutate` resolves as soon as the mutation completes. The triggered refetches happen in parallel. If you want `mutate` to wait until the refetches finish too, set `awaitRefetchQueries`: +:::: composition-api ```ts const { mutate } = useMutation(CREATE_TODO, { refetchQueries: [GET_TODOS], awaitRefetchQueries: true, }) ``` +:::: + +:::: components-api +```vue-html + +``` +:::: ### `onQueryUpdated` For finer control, `onQueryUpdated` intercepts each refetch attempt. Return `false` to skip, `true` to proceed, or a promise to wait for it: +:::: composition-api ```ts const { mutate } = useMutation(CREATE_TODO, { refetchQueries: [GET_TODOS], @@ -194,8 +372,34 @@ const { mutate } = useMutation(CREATE_TODO, { }, }) ``` +:::: + +:::: components-api +```vue + + + +``` +:::: -This is also the way to refetch queries after an `update` callback modifies the cache. See [Cache Updates](/caching/cache-updates#refetching-after-update) for the full pattern. +This is also the way to refetch queries after an `update` callback modifies the cache. See [Cache Updates](/caching/cache-updates#refetch-after-update) for the full pattern. ## Refetching outside components @@ -230,10 +434,10 @@ See the upstream [`client.refetchQueries` reference](https://www.apollographql.c | Goal | Tool | |------|------| -| Pull fresh data once, on a user action (refresh button) | `refetch()` from `useQuery` | +| Pull fresh data once, on a user action (refresh button) | `refetch()` | | Stream updates continuously from the server | [Subscriptions](/data/subscriptions) | -| Keep one query in sync at a fixed cadence | `pollInterval` | -| Update queries after a mutation | `refetchQueries` on `useMutation` | +| Keep one query in sync at a fixed interval | `pollInterval` | +| Update queries after a mutation | `refetchQueries` on the mutation | | Refetch many queries from any context | `client.refetchQueries(...)` | | Avoid the network entirely | [Direct cache updates](/caching/cache-updates) | diff --git a/packages/docs/data/subscriptions.md b/packages/docs/data/subscriptions.md index 51f3e99f..14262275 100644 --- a/packages/docs/data/subscriptions.md +++ b/packages/docs/data/subscriptions.md @@ -1,20 +1,14 @@ # Subscriptions -This page covers GraphQL subscriptions with the [`useSubscription`](/api/composable/functions/useSubscription) composable for real-time updates. +A GraphQL subscription is a long-lived read. Instead of answering once and closing, the server holds the connection open and pushes a new result every time the data changes, so the client learns about it without asking. -## Overview +That makes it the only one of the three operation types the client does not drive. A [query](/data/queries) runs when you ask, a [mutation](/data/mutations) when you call it, and a subscription whenever the server has something to say. -Subscriptions maintain an active connection to your GraphQL server, allowing the server to push updates to the client in real time. - -They are useful for: +Subscriptions are useful for: - **Small, incremental changes to large objects.** Fetch initial state with a query, then receive updates to individual fields as they occur. - **Low-latency, real-time updates.** Chat messages, notifications, live data feeds. -::: tip When to use subscriptions -For most use cases, prefer [polling](/data/refetching#polling) or [refetching on demand](/data/refetching). Reach for subscriptions when you need real-time push updates from the server. -::: - ## Transport setup Subscriptions require a persistent connection. The default `HttpLink` cannot deliver them. Pick one of three transports: @@ -151,7 +145,7 @@ SSE runs over plain HTTP. Pass standard `fetch` headers (auth, etc.) through the ### Multipart HTTP -The default `HttpLink` can also serve subscriptions when the server supports `multipart/mixed` responses. No extra library or configuration is required: Apollo Client adds the right headers when it sees a subscription operation. Support depends on your server (Apollo Router, Yoga, and several others support it). +The default `HttpLink` can also serve subscriptions when the server supports `multipart/mixed` responses. No extra library or configuration is required. Support depends on your server (Apollo Router, Yoga, and several others support it). ## Defining a subscription @@ -177,6 +171,7 @@ const ON_NEW_MESSAGE: TypedDocumentNode< ## Executing a subscription +:::: composition-api ```vue twoslash + + +``` + +The slot gives you `result`, `loading`, `error`, `start`, `stop` and `restart`. + +Without a slot the component renders nothing at all, which is often what you want: a +subscription frequently exists to *cause an effect* rather than to display something. +`@result` is then the whole API. + +```vue-html + +``` +:::: ## Variables +:::: composition-api Subscriptions support the same reactive variable patterns as queries: ```ts twoslash @@ -266,9 +314,31 @@ useSubscription(ON_NEW_MESSAGE, { ``` `debounce` and `throttle` are also available for variable updates, with the same semantics as in [Queries](/data/queries#throttle-and-debounce). +:::: + +:::: components-api +Bind `variables` as a prop. The subscription unsubscribes and resubscribes whenever they +change: + +```vue-html + +``` + +`shouldResubscribe`, `debounce` and `throttle` have no dedicated props; pass them through +`options`: + +```vue-html + +``` +:::: ## Lifecycle control +:::: composition-api Manage the connection imperatively: ```vue twoslash @@ -298,9 +368,41 @@ function reconnect() { ``` +:::: + +:::: components-api +`start`, `stop` and `restart` are all slot props: + +```vue twoslash + + + +``` + +For pausing declaratively, prefer the `disabled` prop below, which survives re-renders. +:::: ### Conditionally enabling +:::: composition-api Use `enabled` to gate the subscription on a condition: ```ts twoslash @@ -319,9 +421,26 @@ const { result } = useSubscription(NOTIFICATIONS, { ``` While `enabled` is `false`, no connection exists. When it flips to `true`, the subscription starts. +:::: + +:::: components-api +Use the `disabled` prop: + +```vue-html + +``` + +While `disabled` is `true`, no connection exists. When it flips to `false`, the subscription +starts. + +As with [``](/data/queries#disabling-queries), the prop is `disabled` rather +than the composable's `enabled`, so that an absent prop means "on". `options: { enabled }` +still works. +:::: ## Event hooks +:::: composition-api ```ts twoslash import { TypedDocumentNode } from '@apollo/client' import { useSubscription } from '@vue/apollo-composable' @@ -347,9 +466,35 @@ onComplete(() => { ``` `onComplete` fires when the server closes the subscription cleanly (for example, after a finite stream like a countdown). +:::: + +:::: components-api +The three hooks are emitted as `@result`, `@error` and `@complete`: + +```vue + + + +``` + +`@complete` fires when the server closes the subscription cleanly (for example, after a +finite stream like a countdown). +:::: ## Subscribing to query updates +:::: composition-api `subscribeToMore` lets you fetch initial data with a query and stream updates into it via a subscription. The merged result behaves like a single, continuously-updated query. ```vue twoslash @@ -414,15 +559,111 @@ watch( ``` The first argument to `updateQuery` (`_prev`) is deprecated in Apollo Client v4. Read from `options.previousData` with the `options.complete` guard for type-safe access. +:::: + +:::: components-api +[``](/api/components/ApolloSubscribeToMore) does this declaratively. +Drop it inside an `` and it subscribes to the query it finds, with no `watch` +and no waiting for the query to load first: + +```vue + + + +``` + +The component renders nothing. It subscribes on mount, unsubscribes on unmount, and +resubscribes when `document`, `variables` or `context` change. Changing `updateQuery` +alone never resubscribes. + +The `context` prop is passed to the link chain for this subscription alone, which is where +per-subscription headers or link options belong: + +```vue-html + +``` + +::: warning It must be inside an `` +`` injects the surrounding query, and throws on mount if there is +none. It can sit anywhere in the default slot, including alongside `#data`. + +The parent query's types are not visible to it, so `updateQuery` is checked against the +subscription's types only. Annotate the callback yourself, as above, to get the parent +query's result type back. +::: + +Errors from the subscription surface on its own `@error` event rather than on the query. See [`SubscribeToMoreOptions`](/api/composable/@vue/namespaces/useQuery/interfaces/SubscribeToMoreOptions) for all available options. +:::: + +:::: composition-api +See [`SubscribeToMoreOptions`](/api/composable/@vue/namespaces/useQuery/interfaces/SubscribeToMoreOptions) for all available options. +:::: ## Options and result reference +:::: composition-api For every available option and method, see: - [`useSubscription.Options`](/api/composable/@vue/namespaces/useSubscription/interfaces/Options) - [`useSubscription.Result`](/api/composable/@vue/namespaces/useSubscription/interfaces/Result) +:::: + +:::: components-api +For every prop, event and slot prop, see: + +- [``](/api/components/ApolloSubscription) +- [``](/api/components/ApolloSubscribeToMore) +- [`useSubscription.Options`](/api/composable/@vue/namespaces/useSubscription/interfaces/Options), for the `options` prop +:::: ## Next steps diff --git a/packages/docs/data/suspense.md b/packages/docs/data/suspense.md index 9b925972..33f1cc02 100644 --- a/packages/docs/data/suspense.md +++ b/packages/docs/data/suspense.md @@ -2,6 +2,16 @@ Vue's built-in `` component lets you display a fallback while async dependencies resolve. Vue Apollo plugs into Suspense by exposing `useQuery` as `PromiseLike`, so you can `await` it in your component's setup. +:::: components-api +::: warning Composition API only +`` cannot suspend. Suspense requires the `await` to happen in the suspending +component's own `setup`. + +Use `await useQuery(...)` in ` diff --git a/packages/docs/data/typescript.md b/packages/docs/data/typescript.md index d0e15e3b..7160bd56 100644 --- a/packages/docs/data/typescript.md +++ b/packages/docs/data/typescript.md @@ -128,6 +128,7 @@ yarn add -D @parcel/watcher Import the `graphql` function from the generated output and define your queries inline: +:::: composition-api ```vue + + +``` + +The components are generic SFCs, so `TData` and `TVariables` flow from the `query` prop into +the slot props and the event payloads. Passing the wrong `variables` shape is a type error, +and `data.project` is checked against the document. + +::: warning Import the components, do not register them globally +Global registration erases the generics, and every slot prop falls back to `any`. The same +applies to a plain `DocumentNode` with no type parameters: annotate it, or generate it, to +keep inference. +::: +:::: The `graphql()` function: @@ -204,6 +258,7 @@ Place this file (for example `apollo-client.d.ts`) somewhere your `tsconfig.json Use `FragmentType` from `@apollo/client` to type component props that receive fragment data: +:::: composition-api ```vue + + +``` + +This pattern: + +- Uses `FragmentType` so the parent must pass a correctly-typed fragment reference. +- Reads the fragment from the cache with [``](/api/components/ApolloFragment), + whose `from` prop takes the masked object straight from the prop. +- Renders `#data` only when every field is present, so `data` is the complete fragment type + rather than a partial one. `#incomplete` covers the rest. +- Works with [Data Masking](/data/data-masking) for isolated component data. +:::: -## Composable return-value shapes +## Result shapes -Vue Apollo's composables expose different result shapes depending on what makes sense for each operation. The table below shows what each composable returns: +The table below shows what each composable returns: | Composable | `current` ref (discriminated union) | Individual refs | |------------|-------------|------| @@ -274,7 +379,17 @@ if (current.value.resultState === 'complete') { The individual refs remain available for code that only reads `loading` or `error` without accessing `result`. -For `useMutation` and `useSubscription` the discriminated union is not provided because their results do not have multiple data states. A mutation is request-response, and a subscription delivers one result at a time. +For `useMutation` and `useSubscription` the discriminated union is not provided because their results do not have multiple data states. + +:::: components-api +::: tip Where the components fit +The components hand these same shapes to their slots. `#data` types its `data` as the +`'complete'` branch, but it renders for `partial` and `streaming` too, so the default slot +is the one that narrows honestly: it receives the whole `current` union and the rules in +the next section apply verbatim inside `v-slot`. A template ref gets the "individual refs" +column with the refs unwrapped. +::: +:::: ## Type narrowing with `resultState` @@ -287,6 +402,7 @@ The `resultState` discriminator narrows `result` precisely. The states for `useQ | `'streaming'` | Data streaming via `@defer` or `@stream` | `TData` | | `'empty'` | No data yet | `undefined` | +:::: composition-api ```ts const { current } = useQuery(GET_USERS, { returnPartialData: true }) @@ -300,6 +416,45 @@ else if (current.value.resultState === 'streaming') { // TData (still arriving) } ``` +:::: + +:::: components-api +The default slot receives the same discriminator, so the narrowing happens in the template: + +```vue twoslash + + + +``` + +Use the default slot whenever the distinction matters. `#data` renders for `'partial'` and +`'streaming'` too, and it types `data` as the complete result in all three cases, so a +partial result reaches it with fields the type claims are there. +:::: For `useFragment` the states are `'complete'` and `'partial'`, and follow the same narrowing pattern. @@ -307,20 +462,36 @@ For `useFragment` the states are `'complete'` and `'partial'`, and follow the sa TypeScript validates required variables and their types: +:::: composition-api ```ts // TypeScript Error: Property 'variables' is missing -const { current } = useQuery(GET_USER_QUERY) +const { current } = useQuery(GET_USER) // TypeScript Error: Property 'id' is missing -const { current } = useQuery(GET_USER_QUERY, { variables: {} }) +const { current } = useQuery(GET_USER, { variables: {} }) // OK -const { current } = useQuery(GET_USER_QUERY, { +const { current } = useQuery(GET_USER, { variables: { id: '1' }, }) ``` When variables are entirely optional (the query has no required variables), the `variables` option itself is optional. +:::: + +:::: components-api +```vue-html + + + + + +``` + +Because `variables` is a prop rather than a required argument, a missing `variables` +altogether is not caught. Everything inside the object is checked, so this only affects the +all-or-nothing case. +:::: ## Manual TypedDocumentNode @@ -343,8 +514,8 @@ const GET_USERS: TypedDocumentNode = gql` ` ``` -::: tip -Always provide the variables type. For queries with no variables, use `Record` so accidental variables produce a type error. +::: tip Always provide the variables type +For queries with no variables, use `Record` so accidental variables produce a type error. ::: ## Next steps diff --git a/packages/docs/guide/index.md b/packages/docs/guide/index.md index 9dfdb5f4..660224e3 100644 --- a/packages/docs/guide/index.md +++ b/packages/docs/guide/index.md @@ -6,9 +6,22 @@ Ready to try it out? Skip to the Installation. +## Two APIs, one library + +Vue Apollo ships two packages, and the selector at the top of the sidebar switches this +guide between them: + +| Package | What you write | Reach for it when | +|---|---|---| +| [`@vue/apollo-composable`](/api/composable/) | `useQuery` and friends in ` + + +``` +:::: ## Compatibility @@ -52,6 +103,8 @@ const { current } = useQuery(gql` | Vue | 3.5+ | | Apollo Client | 4.1+ | +Vue 2 is no longer supported. See [What's changed in v5](/migration/whats-changed). + ::: warning Apollo Client 4.1 required Vue Apollo requires `@apollo/client` version 4.1.0 or higher. The features Vue Apollo depends on (improved TypeScript inference, the `DataState` discriminated union, incremental delivery handlers) are only available from 4.1 onward. ::: diff --git a/packages/docs/guide/installation.md b/packages/docs/guide/installation.md index 3b607f49..e54e1171 100644 --- a/packages/docs/guide/installation.md +++ b/packages/docs/guide/installation.md @@ -4,6 +4,7 @@ Install Apollo Client and Vue Apollo: +:::: composition-api ::: code-group ```shell [npm] @@ -19,11 +20,40 @@ pnpm add @apollo/client @vue/apollo-composable@next graphql ``` ::: +:::: + +:::: components-api +`@vue/apollo-components` lists `@vue/apollo-composable` as a peer dependency, so install +both. + +::: code-group + +```shell [npm] +npm install @apollo/client @vue/apollo-composable@next @vue/apollo-components@next graphql +``` + +```shell [yarn] +yarn add @apollo/client @vue/apollo-composable@next @vue/apollo-components@next graphql +``` + +```shell [pnpm] +pnpm add @apollo/client @vue/apollo-composable@next @vue/apollo-components@next graphql +``` + +::: +:::: ::: warning Pre-release version Vue Apollo v5 is currently in pre-release. The `@next` tag installs the latest pre-release build. ::: +::: tip Choosing an API +The selector at the top of the sidebar switches every example in the guide between the +**Composition API** (`useQuery` and friends) and the **Components API** (`` +and friends). They are the same library, so you can pick per component and change your +mind later. See the [API overview](/api/) for how to choose. +::: + ## Step 2: Create an Apollo Client Create a file to configure your Apollo Client instance: @@ -68,12 +98,20 @@ app.provide(DefaultApolloClient, apolloClient) app.mount('#app') ``` +:::: composition-api That's it. You can now use [`useQuery`](/api/composable/functions/useQuery.md), [`useMutation`](/api/composable/functions/useMutation.md), and the other composables in any component. +:::: + +:::: components-api +That's it. You can now use [``](/api/components/ApolloQuery), +[``](/api/components/ApolloMutation) and the rest in any template. +:::: ## Step 4: Your first query Verify the setup with a simple query: +:::: composition-api ```vue twoslash + + +``` +:::: ## IDE integration diff --git a/packages/docs/guide/why-apollo.md b/packages/docs/guide/why-apollo.md index 38ed64c0..5ab9efcb 100644 --- a/packages/docs/guide/why-apollo.md +++ b/packages/docs/guide/why-apollo.md @@ -6,6 +6,7 @@ Apollo Client is a comprehensive state-management library for JavaScript. With G You write a query, and Apollo Client handles fetching, caching, and updating the UI: +:::: composition-api ```vue twoslash ``` +:::: + +:::: components-api +```vue twoslash + + + +``` +:::: You do not need to track loading states by hand, juggle error branches, or update the cache after every mutation. Apollo Client handles all of it. @@ -44,7 +76,10 @@ Because the cache is normalized by `__typename` + `id`, any query that reads an ## Vue-native reactivity -Vue Apollo plugs into Vue's reactivity system. Query variables can be refs, reactive objects, getters, or even per-key reactive maps: +Vue Apollo plugs into Vue's reactivity system. + +:::: composition-api +Query variables can be refs, reactive objects, getters, or even per-key reactive maps: ```vue twoslash + + +``` + +When `userId` changes, the query re-executes with the new value. No refs inside the object, no getters, no watcher boilerplate. +:::: ## TypeScript support @@ -77,7 +150,6 @@ With [GraphQL Codegen](/data/typescript), every query, mutation, and result is f ```ts twoslash import type { TypedDocumentNode } from '@apollo/client' -import { useQuery } from '@vue/apollo-composable' declare const graphql: (q: string) => TypedDocumentNode<{ user: { id: string, name: string, email: string } }, { id: string }> @@ -91,11 +163,15 @@ const UserQuery = graphql(` } } `) - -const { current } = useQuery(UserQuery, { variables: { id: '1' } }) ``` +:::: composition-api `current.result` is typed precisely, including narrowing by `current.resultState` so partial and streaming states are handled safely. +:::: + +:::: components-api +Passing that document to `` types its `variables` prop, its slot props and its event payloads, and `#data` hands you the fully-resolved shape with no narrowing to write. +:::: ## When to use Apollo Client @@ -106,4 +182,6 @@ Apollo Client is a good fit when: - You want real-time updates through subscriptions, `@defer`, or `@stream`. - You value TypeScript correctness throughout the stack. -For simple REST APIs or apps that do little caching, lighter alternatives exist. For GraphQL apps with non-trivial caching needs, Apollo Client is the most complete option. +If your backend speaks REST rather than GraphQL, [rstore](https://rstore.dev/) covers much of the same ground for Vue and Nuxt. + +For apps that do little caching, something lighter still may be enough. For GraphQL apps with non-trivial caching needs, Apollo Client is the most complete option. diff --git a/packages/docs/index.md b/packages/docs/index.md index b613687b..6bf67042 100644 --- a/packages/docs/index.md +++ b/packages/docs/index.md @@ -13,13 +13,13 @@ hero: link: /guide/ - theme: alt text: API Reference - link: /api/composable/ + link: /api/ features: - title: Automatic updates details: Don't think about updating the UI or refetching the queries! icon: ✨ -- title: Supports all Vue APIs - details: Option API, Composition API or Components +- title: Composables or components + details: useQuery in script, or ApolloQuery in the template. Same client, same cache. icon: 🧩 - title: SSR-ready details: Run your queries on the server before rendering the page HTML diff --git a/packages/docs/local-state/overview.md b/packages/docs/local-state/overview.md index 123c0143..bbb1d509 100644 --- a/packages/docs/local-state/overview.md +++ b/packages/docs/local-state/overview.md @@ -19,6 +19,7 @@ export const theme = ref<'light' | 'dark'>('light') ```ts import { defineStore } from 'pinia' +import { ref } from 'vue' export const useAuthStore = defineStore('auth', () => { const token = ref(null) @@ -63,14 +64,14 @@ Apollo Client provides [`makeVar`](https://www.apollographql.com/docs/react/loca Vue Apollo intentionally does not ship a `useReactiveVar` composable, for two reasons: -1. Vue already has a complete reactivity system (`ref`, `computed`, `watch`). It does not need a parallel one. +1. Vue already has a complete reactivity system (`ref`, `computed`, `watch`). 2. For nearly all use cases, a Vue ref or Pinia store is a better fit than `makeVar`. If you do need a reactive variable for Apollo field policies, you can read it from Vue by subscribing manually: ```ts import { makeVar } from '@apollo/client/cache' -import { onScopeDispose, ref, shallowRef } from 'vue' +import { onScopeDispose, shallowRef } from 'vue' export function useApolloReactiveVar(rv: ReturnType>) { const state = shallowRef(rv()) @@ -89,7 +90,7 @@ But unless you have a specific reason to keep state inside Apollo's cache layer When you do go the Apollo local-state route, the two mechanisms are: -- **Field policies with `read` functions.** Configure a type policy whose `read` returns the local value. Most flexible, no resolver overhead, recommended. +- **Field policies with `read` functions.** Configure a type policy whose `read` returns the local value. Most flexible, and the recommended approach. - **Local resolvers.** Implement resolvers for `@client` fields the same way you would for a server. Older approach; Apollo Client v4 still supports it but field policies are now preferred. Both are documented in detail in the Apollo docs: @@ -105,7 +106,7 @@ From a Vue Apollo perspective, both work the same: query as usual, and `@client` |------|------| | Component-local UI state (open/closed, active tab) | Vue ref | | App-wide state shared across components (auth, theme) | Pinia store | -| Server data with caching | `useQuery` | +| Server data with caching | A query | | Client-only field accessed through a GraphQL query | Apollo field policy with `read` | | Client-only field accessed everywhere except GraphQL | Vue ref or Pinia store | diff --git a/packages/docs/migration/compat.md b/packages/docs/migration/compat.md index df8b425e..0e61637e 100644 --- a/packages/docs/migration/compat.md +++ b/packages/docs/migration/compat.md @@ -4,6 +4,12 @@ For a step-by-step migration that uses this layer, see the [Migration guide](/migration/guide). For a high-level summary of the v4 to v5 changes, see [What's changed in v5](/migration/whats-changed). +::: warning Composables only +`@vue/apollo-components` has no compat entry point. Its v5 rewrite changes props, slots and +events with no shim, so those changes have to be made up front. See +[Migrating v4 components](/migration/components). +::: + ## Purpose Vue Apollo v5 introduced breaking changes to nearly every composable's call signature (variables moved into options, `mutate(variables)` became `mutate({ variables })`, the error type changed). Migrating a large codebase line-by-line in one shot is painful. The compat layer keeps the v4 surface compilable while you do the rest of the upgrade incrementally. @@ -32,7 +38,7 @@ The compat module re-exports everything from the main entry, so a single find-re | `useQuery` | `useQuery(doc)`, `useQuery(doc, vars)`, `useQuery(doc, vars, options)`, `useQuery(doc, undefined, options)` | Variables can be positional or in options. | | `useSubscription` | Same 3-arg form as `useQuery` | Variables can be positional or in options. | | `useMutation` | `useMutation(doc, options?)`. `mutate(variables, overrides)` keeps the v4 two-arg form. | The composable signature was already compatible; the call signature is wrapped. | -| `useLazyQuery` | `useLazyQuery(doc, vars?, options?)`. `load(document?, variables?)` keeps v4's two-arg form and first-call-only semantics. | See [`useLazyQuery` notes](#uselazyquery) below. | +| `useLazyQuery` | `useLazyQuery(doc, vars?, options?)`. `load(document?, variables?)` keeps v4's two-arg form and first-call-only semantics. | See [`useLazyQuery` notes](#uselazyquery-notes) below. | ### Return-shape wrapping @@ -211,4 +217,4 @@ There is no deprecation timeline on the compat module itself, but every new feat - [Migration guide](/migration/guide) is the prescriptive walkthrough. - [What's changed in v5](/migration/whats-changed) is the high-level reference. -- [TypeScript: Composable return-value shapes](/data/typescript#composable-return-value-shapes) compares v5 native composable shapes. +- [TypeScript: Result shapes](/data/typescript#result-shapes) compares v5 native composable shapes. diff --git a/packages/docs/migration/components.md b/packages/docs/migration/components.md new file mode 100644 index 00000000..d8798e5a --- /dev/null +++ b/packages/docs/migration/components.md @@ -0,0 +1,310 @@ +# Migrating v4 components + +v4 shipped `@vue/apollo-components`, a set of renderless components built on `@vue/apollo-option` and its `this.$apollo` smart-query layer. v5 keeps the package and the element names, and rebuilds them on top of [`@vue/apollo-composable`](/api/composable/). + +This page lists what moved. + +::: warning No compat layer for components +[`@vue/apollo-composable/compat`](/migration/compat) covers the composables only. The +components are a from-scratch rewrite with no v4-signature shim, so the changes here have +to be made before the app runs. + +The changes are mechanical and mostly confined to templates, so a codebase-wide search for +` + +``` + +Global registration loses the generic slot-prop types, so `data` in `#data` falls back to `any`. Import the components where you use them to keep them typed. + +## Step 4: The wrapper element is gone + +Every v4 component rendered a `
` around its slot content, configurable with the `tag` prop. The v5 components render exactly what their slots return and nothing else. + +```vue-html + + + ... + +``` + +Delete any `tag` prop, and check styling that relied on the wrapper. If you need one back, write it yourself: + +```vue-html +
+ + ... + +
+``` + +::: tip Multiple roots +Because the components are renderless, an [``](/api/components/ApolloQuery) +that renders several elements makes its parent a multi-root component. That is fine in +Vue 3, but attribute fallthrough stops working, so wrap it if the parent passes `class` or +`style` down. +::: + +## `` + +### Props + +| v4 | v5 | +|---|---| +| `query` (document or `gql => document`) | `query`, a document only. Use a `TypedDocumentNode` to get typed slots. | +| `variables` | `variables`, typed from the document | +| `skip` | `disabled` | +| `fetchPolicy` | Same name | +| `pollInterval` | Same name | +| `debounce`, `throttle` (default `0`) | Same names, no default. Absent means no delay. | +| `clientId` | Same name | +| `update` (transform the data) | Removed. Transform inside the slot, or with a cache field policy. | +| `notifyOnNetworkStatusChange`, `context`, `deep`, `prefetch` | Through `options`, where the option still exists in Apollo Client v4 | +| `tag` | Removed, see above | +| `options` | `options`, now the whole [`useQuery.Options`](/api/composable/@vue/namespaces/useQuery/interfaces/Options) object | + +`skip` to `disabled` is a rename only; the meaning is identical. See [Disabling queries](/data/queries#disabling-queries). + +### Slots + +v4 had one slot, handing you a result object to branch on yourself. v5 adds named slots that do the branching, and keeps a default slot for when you would rather do it yourself: + +```vue-html + + +
Loading...
+
{{ error.message }}
+
    +
  • {{ dog.breed }}
  • +
+
+``` + +```vue-html + + + + + + +``` + +`#empty` is new, and it is the last branch in the chain: a query that settles with no result, no error and nothing in flight renders it where v4 rendered nothing. Add the `empty` prop, a predicate over the result, to also catch results that arrived but count as empty, such as a zero-length list. A `disabled` query is the exception and still renders nothing at all. + +The slot-prop names changed too: + +| v4 slot prop | v5 | +|---|---| +| `result.data` | `data` in `#data`, never `undefined` there. `result` in the default slot. | +| `result.loading` | `loading`, in `#data` and in the default slot | +| `result.error` | `error`, in `#error`, `#data` and the default slot. In `#data` it means the query failed but there is still something to show. | +| `result.networkStatus` | `networkStatus`, default slot only | +| `result.fullData` | Removed, along with the `update` prop it existed for | +| `isLoading` | `loading` | +| `gqlError` | Removed. Narrow with `CombinedGraphQLErrors.is(error)`, see [Error Handling](/data/error-handling#identifying-error-types). | +| `times` | Removed | +| `query` | On the exposed result, through a template ref | + +A straight port keeps the `v-if` chain and uses only the default slot, which is a valid v5 form. Moving to the named slots is worth doing though: `data` is non-nullable inside `#data`, so the optional chaining v4 needed disappears. [Two ways to read the result](/data/queries#two-ways-to-read-the-result) covers both modes. + +### Events + +| v4 | v5 | +|---|---| +| `@loading="isLoading => ..."` | Removed. Read the `loading` slot prop. | +| `@result="result => ..."`, receiving `{ data, loading, error, ... }` | `@result="data => ..."`, receiving the data alone. `@nextState` gives the whole state. | +| `@error="error => ..."` | Same name. The payload is an `ErrorLike` rather than a v3 `ApolloError`. | + +v5 also emits `@completeResult`, `@partialResult` and `@streamingResult`. See [Event hooks](/data/queries#event-hooks). + +### Retaining results between variable changes + +v4's `` merged the previous data into the new result while loading, so a variables change never blanked the list. v5 does not do this unless asked: + +```vue-html + +``` + +The retained result is flagged with the `isPreviousResult` slot prop, which v4 had no equivalent for, so a stale list can be styled as stale. See [Keeping previous data](/data/queries#keeping-previous-data). + +## `` + +### Props + +| v4 | v5 | +|---|---| +| `mutation` (document or `gql => document`) | `mutation`, a document only | +| `variables` | `variables`, typed from the document | +| `clientId` | Same name | +| `optimisticResponse`, `update`, `refetchQueries`, `context` | Through `options`, typed as [`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options) | +| `tag` | Removed | + +```vue-html + + +``` + +```vue-html + + +``` + +### `mutate()` + +[``](/api/components/ApolloMutation)'s `mutate` slot prop now takes an options object rather than bare variables, matching `useMutation`: + +```vue-html + + +``` + +```vue-html + + +``` + +Calling it with no arguments still uses the `variables` prop, as in v4. + +### Slot props and events + +`mutate`, `loading` and `error` are unchanged. `gqlError` is gone, for the same reason as on ``. v5 adds `called`, `result` and `reset`. + +`@done` and `@error` keep their names. `@loading` is gone; use the `loading` slot prop. + +v4's `mutate` caught every error and resolved with `undefined`. v5's rejects instead: `throws` defaults to `'auto'`, and the component bridges `@error` to `useMutation`'s `onError` only while the parent is actually listening, so with nothing bound there is no handler and the promise rejects. A v4 call site that never expected a rejection needs one of two things: bind `@error`, which makes `mutate()` resolve again and delivers the failure to the event, or set `:options="{ throws: 'never' }"` and read the `error` slot prop. See [Error throwing behavior](/data/mutations#error-throwing-behavior). + +## `` + +[``](/api/components/ApolloSubscribeToMore) keeps its name and its placement: it still takes `document`, `variables` and `updateQuery`, and still has to sit inside an ``. + +There is one addition: a `context` prop, passed to the link chain for this subscription. v4 had no equivalent, so there is nothing to migrate; it is there for headers and link state that only the subscription needs. + +One difference: + +- Changing `updateQuery` alone does not re-subscribe. Only `document`, `variables` and `context` do. + +```vue-html + + + + +``` + +Note that it now lives in the default slot rather than beside a single slot's content. The default slot renders in both modes. + +## New in v5 + +Two components have no v4 equivalent: + +- [``](/api/components/ApolloSubscription) runs a standalone subscription, which v4 could only do through `this.$apollo.addSmartSubscription`. See [Subscriptions](/data/subscriptions). +- [``](/api/components/ApolloFragment) reads a fragment from the cache. It is what makes the components usable with [data masking](/data/data-masking), where a component receives a masked object and has to unmask the fields it owns. See [Fragments](/data/fragments). + +## What has no component form + +- **Lazy queries.** See [Lazy Queries](/advanced/lazy-queries); `disabled` covers the "wait for variables" case. +- **Suspense.** See [Suspense](/data/suspense). +- **Aggregate loading counters.** `useQueryLoading` and friends are scoped per component instance, so they cannot see a child ``. See [Loading States](/advanced/loading-states). + +The two APIs share one client and one cache, so mixing them is expected. Reach for ` + + +``` -Each call to `loadMore` passes the cursor from the previous page. Apollo merges the new items into the cached list using the `merge` function configured above. +`#data` only renders once there is a result, so `data.feed.hasMore` and +`data.feed.nextCursor` are safe to read without checking for one first. +:::: + +Each `fetchMore` call passes the cursor from the previous page. Apollo merges the new items into the cached list using the `merge` function configured above. ## Relay-style connections @@ -157,13 +184,9 @@ The helper handles `edges`, `pageInfo`, and the standard cursor naming. Usage: -```vue twoslash +:::: composition-api +```vue + + +``` +:::: ## Inserting new items into a paginated list @@ -220,6 +270,27 @@ The cache cannot tell which page a newly-created item belongs to, so a mutation See [Cache Updates](/caching/cache-updates) for the insertion patterns. +:::: components-api +[``](/api/components/ApolloMutation) has props only for `mutation`, +`variables` and `clientId`. `update`, `refetchQueries` and `optimisticResponse` all reach +`useMutation` through the `options` prop, which takes the full +[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options) +object: + +```vue-html + + + +``` +:::: + ## Key arguments If your paginated field also accepts filter or sort arguments, mark those as `keyArgs` so each filter value gets its own merged list: diff --git a/packages/docs/pagination/offset-based.md b/packages/docs/pagination/offset-based.md index f7a1ec44..136cc21e 100644 --- a/packages/docs/pagination/offset-based.md +++ b/packages/docs/pagination/offset-based.md @@ -45,13 +45,9 @@ The helper defines a `merge` function that concatenates pages as they arrive, so ## Loading more with `fetchMore` -```vue twoslash +:::: composition-api +```vue + + +``` +:::: -`fetchMore` uses the original query and variables, overridden by what you pass. With the `offsetLimitPagination` helper installed, the new page is appended to the cached list automatically. `current.result.feed` updates to include every item received so far. +`fetchMore` uses the original query and variables, overridden by what you pass. With the `offsetLimitPagination` helper installed, the new page is appended to the cached list automatically. The rendered list grows to include every item received so far. ## Key arguments @@ -112,16 +135,11 @@ See [Apollo's `keyArgs` reference](https://www.apollographql.com/docs/react/pagi ## Reactive variables -To use offset-based pagination with reactive variables (page-number UI), put `offset` and `limit` into refs: +To use offset-based pagination with reactive variables (page-number UI), drive `offset` from a ref: -```vue twoslash +:::: composition-api +```vue + + +``` + +This pattern replaces the page rather than appending. With `keepPreviousResult`, the +previous page stays on screen while the new one loads, marked `isPreviousResult` so you can +dim it until the new page lands. It is off by default. See +[Keeping previous data](/data/queries#keeping-previous-data). +:::: ## Refreshing all pages When you need to refresh the entire merged list (for example after a server-side reorder), refetch the original query: +:::: composition-api ```ts const { refetch } = useQuery(FEED_QUERY, { variables: { offset: 0, limit: 10 }, @@ -166,6 +226,17 @@ const { refetch } = useQuery(FEED_QUERY, { await refetch() ``` +:::: + +:::: components-api +```vue-html + +``` +:::: The cache is rebuilt from the new response. diff --git a/packages/docs/pagination/overview.md b/packages/docs/pagination/overview.md index fc343eca..8eac5add 100644 --- a/packages/docs/pagination/overview.md +++ b/packages/docs/pagination/overview.md @@ -58,16 +58,11 @@ The `merge` function in the field policy decides how a new page combines with wh ## Loading the next page +:::: composition-api Vue Apollo's [`useQuery`](/api/composable/functions/useQuery) returns a `fetchMore` function for loading the next page: -```vue twoslash +```vue + + +``` + +The `variables` prop stays at the *first* page throughout. `fetchMore` does not change it. +:::: When you call `fetchMore`, Apollo: 1. Sends a query with the new variables. 2. Merges the result with the cached value using the field policy's `merge` function. -3. Notifies any active query that reads the field, including this one. The `current.result` ref updates with the merged list. +3. Notifies any active query that reads the field, including this one. The rendered list updates with the merged value. You typically want the `offsetLimitPagination` helper (or a cursor equivalent) configured for the field, otherwise `merge` defaults to replacing the cached value with the new page. ## With reactive variables -If you keep pagination state in a Vue ref, `useQuery` re-executes when it changes. This is useful for "paginate by setting the page number" UIs: +If you keep pagination state in a Vue ref, the query re-executes when it changes. This is useful for "paginate by setting the page number" UIs: -```vue twoslash +:::: composition-api +```vue + + +``` + +For paginated lists you almost always want `keepPreviousResult`, so the list does not +blink to empty between pages. It is off by default. See +[Keeping previous data](/data/queries#keeping-previous-data). +:::: ::: tip `fetchMore` vs reactive variables Use `fetchMore` when you want to grow an in-place list (infinite scroll, "load more" button). Use reactive variables when each page replaces the previous one (numbered pagination, "next page" navigation). @@ -164,6 +229,27 @@ After a mutation adds or removes an item from a paginated list, the cache does n For lists where order matters and the server controls it (chronological feeds, server-side sort), `refetchQueries` is usually more reliable. +:::: components-api +[``](/api/components/ApolloMutation) has props only for `mutation`, +`variables` and `clientId`. Both `update` and `refetchQueries` reach `useMutation` through +the `options` prop, which takes the full +[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options) +object: + +```vue-html + + + +``` +:::: + ## Next steps - [Offset-based](/pagination/offset-based) details the offset/limit pattern. diff --git a/packages/docs/scripts/checkApiFlavors.ts b/packages/docs/scripts/checkApiFlavors.ts new file mode 100644 index 00000000..3849e0ca --- /dev/null +++ b/packages/docs/scripts/checkApiFlavors.ts @@ -0,0 +1,125 @@ +/** + * Lints the `:::: -api` containers across the guide. + * + * An unbalanced container fails silently: markdown-it renders the stray marker as text and + * the page still builds. Headings are rejected inside a block because VitePress builds the + * page outline from the DOM, so a hidden heading scrolls nowhere for the other flavor. + */ + +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { API_FLAVORS } from '../.vitepress/apiFlavors.ts' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const docsDir = path.resolve(__dirname, '..') + +const IGNORED_DIRS = new Set(['node_modules', '.vitepress', 'public']) + +/** Generated output. `api/index.md` is hand written, so `api` itself stays in scope. */ +const IGNORED_PATHS = new Set(['api/composable', 'api/components']) + +const FLAVORS = API_FLAVORS.map(flavor => flavor.value).join('|') + +// markdown-it-container opens at three colons and closes on a run at least as long, so the +// opening length has to be carried to the close rather than assumed. +const OPEN = new RegExp(`^(:{3,})\\s*(?:${FLAVORS})-api(?:\\s+\\S.*)?$`) +const CLOSE = /^(:{3,})\s*$/ +// The marker run is greedy and the tail cannot start with a fence character, so there is +// only one way to split a fence line. +const FENCE = /^\s*(`+|~+)([^`~].*)?$/ + +function* markdownFiles(dir: string): Generator { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) { + continue + } + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + if (!IGNORED_PATHS.has(path.relative(docsDir, full))) { + yield* markdownFiles(full) + } + } + else if (entry.name.endsWith('.md')) { + yield full + } + } +} + +const problems: string[] = [] + +for (const file of markdownFiles(docsDir)) { + const relative = path.relative(docsDir, file) + const lines = fs.readFileSync(file, 'utf-8').split('\n') + + let openedAt: number | null = null + let openLength = 0 + let fence: { char: string, length: number } | null = null + + lines.forEach((line, index) => { + const fenceMatch = FENCE.exec(line) + if (fenceMatch != null && fenceMatch[1]!.length >= 3) { + const marker = fenceMatch[1]! + if (fence == null) { + fence = { char: marker[0]!, length: marker.length } + } + else if (marker[0] === fence.char && marker.length >= fence.length && (fenceMatch[2] ?? '').trim() === '') { + fence = null + } + return + } + if (fence != null) { + return + } + + const openMatch = OPEN.exec(line) + if (openMatch != null) { + const length = openMatch[1]!.length + if (length < 4) { + problems.push(`${relative}:${index + 1} opens a flavor block with ${length} colons; use four so it can wrap \`:::\` containers`) + } + if (openedAt != null) { + problems.push(`${relative}:${index + 1} opens a flavor block while one from line ${openedAt} is still open`) + } + openedAt = index + 1 + openLength = length + return + } + + const closeMatch = CLOSE.exec(line) + if (closeMatch != null) { + const length = closeMatch[1]!.length + if (openedAt == null) { + if (length >= 4) { + problems.push(`${relative}:${index + 1} closes a flavor block that was never opened`) + } + return + } + // A shorter run belongs to a nested `::: tip`, exactly as markdown-it resolves it. + if (length >= openLength) { + openedAt = null + } + return + } + + if (openedAt != null && /^#{1,6}\s/.test(line)) { + problems.push(`${relative}:${index + 1} puts a heading inside a flavor block; move it above the \`::::\``) + } + }) + + if (openedAt != null) { + problems.push(`${relative}:${openedAt} opens a flavor block that is never closed`) + } +} + +if (problems.length > 0) { + console.error(`Found ${problems.length} API flavor problem(s):\n`) + for (const problem of problems) { + console.error(` ${problem}`) + } + process.exitCode = 1 +} +else { + console.log('API flavor containers are balanced.') +} diff --git a/packages/docs/scripts/generateComponentApi.ts b/packages/docs/scripts/generateComponentApi.ts new file mode 100644 index 00000000..a133800e --- /dev/null +++ b/packages/docs/scripts/generateComponentApi.ts @@ -0,0 +1,240 @@ +/** + * Generates the API reference for `@vue/apollo-components`. + * + * api-extractor and typedoc both work off `.d.ts`, which for an SFC is the compiler's + * `__VLS_` shape rather than anything readable. `vue-component-meta` runs the same + * analysis `vue-tsc` does, so props, events and slots come out with their JSDoc attached. + */ + +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { createChecker } from 'vue-component-meta' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const docsDir = path.resolve(__dirname, '..') +const componentsDir = path.resolve(docsDir, '../vue-apollo-components') +const outDir = path.join(docsDir, 'api/components') + +const COMPONENTS = [ + 'ApolloQuery', + 'ApolloMutation', + 'ApolloSubscription', + 'ApolloSubscribeToMore', + 'ApolloFragment', +] as const + +/** + * The virtual file the SFC compiles to aliases its imports, so a composable namespace + * prints as `u.Options`. Each component imports exactly one composable, which makes the + * expansion unambiguous. + */ +function composableOf(source: string): string | undefined { + return source.match(/import \{ (use\w+) \} from '@vue\/apollo-composable'/)?.[1] +} + +/** + * `NoInfer` marks a prop as a non-inference site. It says nothing about what may be passed, + * so it would only puzzle a reader of the table. + */ +function dropNoInfer(type: string): string { + return type.replace(/\bNoInfer<((?:[^<>]|<[^<>]*>)+)>/g, '$1') +} + +/** The rolled-up barrel abbreviates its exports; `R` is the public `RenameKey`. */ +const BARREL_ALIASES: Record = { R: 'RenameKey' } + +/** Short real namespaces the import-alias rewrite below must not swallow. */ +const NOT_AN_ALIAS = /^(?:Map|Set|JSX|Vue|Ref)\.$/ + +function resolveAliases(type: string, source: string): string { + const resolved = dropNoInfer(type) + .replace(/\b([A-Z])(?=<)/g, (match, alias: string) => BARREL_ALIASES[alias] ?? match) + const composable = composableOf(source) + if (composable == null) { + return resolved + } + return resolved.replace(/\b[A-Za-z_$]{1,3}\.(?=[A-Z])/g, match => + NOT_AN_ALIAS.test(match) ? match : `${composable}.`) +} + +/** + * Inlines a local `type X = ...` declared in the SFC. + * + * `vue-component-meta` prints such an alias by name, and the reader has no file to look it + * up in. Long declarations stay abbreviated; expanding them helps nobody. + */ +function resolveLocalAliases(type: string, source: string): string { + let resolved = type + + for (let pass = 0; pass < 2; pass++) { + resolved = resolved.replace(/\b[A-Z][\w$]*\b/g, (name) => { + const declaration = new RegExp(`^type ${name} =([\\s\\S]*?)\\n(?=\\n|type |const |interface )`, 'm').exec(source) + const inlined = declaration?.[1] + ?.replace(/\s+/g, ' ') + .replace(/<\s+/g, '<') + .replace(/\s+>/g, '>') + .replace(/\s+,/g, ',') + .trim() + return inlined != null && inlined.length <= 160 ? inlined : name + }) + } + + return resolved +} + +/** Placeholder for a table cell with nothing to say, kept visually quieter than a dash. */ +const EMPTY = ' ' + +function cell(value: string | undefined) { + if (!value) { + return EMPTY + } + // A table cell cannot hold a real paragraph break, so keep it as a visible one. + return value + .replace(/\|/g, '\\|') + .replace(/\r?\n\s*\n/g, '

') + .replace(/\r?\n/g, ' ') +} + +function code(value: string | undefined) { + return value ? `\`${cell(value)}\`` : EMPTY +} + +interface SlotProp { name: string, type: string } + +/** `vue-component-meta` reports a slot that takes nothing as `any`. */ +function takesNoProps(slot: { type?: string }): boolean { + return slot.type == null || slot.type === 'any' +} + +function slotProps(schema: unknown): SlotProp[] { + if (typeof schema !== 'object' || schema == null) { + return [] + } + const resolved = schema as { kind?: string, schema?: Record } + if (resolved.kind !== 'object' || resolved.schema == null) { + return [] + } + return Object.entries(resolved.schema).map(([name, value]) => ({ + name, + type: value.type ?? 'unknown', + })) +} + +function render(name: string, meta: ReturnType['getComponentMeta']>, source: string) { + const expand = (type: string) => resolveAliases(resolveLocalAliases(type, source), source) + const composable = composableOf(source) + const lines: string[] = [ + `# ${name}`, + '', + '', + '', + ] + + const props = meta.props.filter(prop => !prop.global) + if (props.length > 0) { + lines.push('## Props', '', '| Name | Type | Default | Description |', '| --- | --- | --- | --- |') + for (const prop of props) { + const required = prop.required ? ' *(required)*' : '' + lines.push( + `| \`${prop.name}\`${required} | ${code(expand(prop.type))} | ${code(prop.default === 'undefined' ? undefined : prop.default)} | ${cell(prop.description)} |`, + ) + } + lines.push('') + } + + if (meta.events.length > 0) { + lines.push('## Events', '', '| Name | Payload |', '| --- | --- |') + for (const event of meta.events) { + lines.push(`| \`${event.name}\` | ${code(expand(event.type))} |`) + } + lines.push('') + } + + if (meta.slots.length > 0) { + lines.push('## Slots', '') + for (const slot of meta.slots) { + lines.push(`### \`#${slot.name}\``, '') + if (slot.description) { + lines.push(slot.description, '') + } + const scoped = slotProps(slot.schema) + if (scoped.length > 0) { + lines.push('| Slot prop | Type |', '| --- | --- |') + for (const prop of scoped) { + lines.push(`| \`${prop.name}\` | ${code(expand(prop.type))} |`) + } + } + else if (takesNoProps(slot)) { + lines.push('No slot props.') + } + else { + // A union of shapes has no single table, so name the type rather than claim none. + const type = expand(slot.type) + lines.push(`Receives ${code(type)}.`) + if (composable != null && type.includes(`${composable}.Current`)) { + lines.push( + '', + `Every field of [\`${composable}.Current\`](/api/composable/@vue/namespaces/${composable}/interfaces/Current) is present, so \`resultState\` narrows \`result\` here exactly as it does in script.`, + ) + } + } + lines.push('') + } + } + + // `vue-component-meta` cannot read `defineExpose` from a generic SFC, and the exposed + // surface is the composable's result anyway, so link it rather than duplicate it. + if (composable != null) { + lines.push( + '## Exposed', + '', + `A template ref receives the full [\`${composable}.Result\`](/api/composable/@vue/namespaces/${composable}/interfaces/Result) surface, with refs unwrapped.`, + '', + '```vue', + '', + '```', + '', + ) + } + + return `${lines.join('\n').trimEnd()}\n` +} + +function generate() { + const checker = createChecker(path.join(componentsDir, 'tsconfig.lib.json'), { + forceUseTs: true, + schema: { ignore: [] }, + }) + + fs.mkdirSync(outDir, { recursive: true }) + + for (const name of COMPONENTS) { + const file = path.join(componentsDir, 'src', `${name}.vue`) + const source = fs.readFileSync(file, 'utf-8') + const meta = checker.getComponentMeta(file) + fs.writeFileSync(path.join(outDir, `${name}.md`), render(name, meta, source)) + console.log(` Generated: ${path.relative(docsDir, path.join(outDir, `${name}.md`))}`) + } + + const index = [ + '# Components API', + '', + '', + '', + ...COMPONENTS.map(name => `- [${name}](./${name})`), + '', + ].join('\n') + fs.writeFileSync(path.join(outDir, 'index.md'), index) +} + +console.log('Generating @vue/apollo-components API reference...') +generate() + +if (process.exitCode == null) { + console.log(`Total: ${COMPONENTS.length} components`) +} diff --git a/packages/docs/ssr/nuxt.md b/packages/docs/ssr/nuxt.md index 9dbc3869..a12b387c 100644 --- a/packages/docs/ssr/nuxt.md +++ b/packages/docs/ssr/nuxt.md @@ -89,10 +89,11 @@ What this does: - The `app:rendered` hook serializes the cache into Nuxt's payload after server rendering. - `ssrForceFetchDelay` lets cached queries revalidate from the network shortly after hydration, useful for slightly stale data. -## Step 3: Use the composables +## Step 3: Fetch some data In any page or component: +:::: composition-api ```vue + + +``` + +`` registers an `onServerPrefetch` hook automatically, so Nuxt waits for the +data before rendering the page. The result is in HTML, and the client hydrates without a +re-fetch. + +Nuxt auto-imports do not cover this package, so import the components explicitly. +:::: ## Awaiting queries with Suspense @@ -137,6 +169,16 @@ const { current } = await useQuery(gql` Nuxt wraps pages in `` automatically, so this works without extra setup. See [Suspense](/data/suspense) for the full pattern. +:::: components-api +::: warning Composition API only +`` cannot suspend, so a page built from it renders through its slots rather +than blocking. Nuxt still waits for the data. + +Use `await useQuery(...)` in ` + + +``` + +The check has to live in ` + + +``` + +Import the components directly, as above. Global registration is available but loses the +generic slot-prop types: + +```ts +import { VueApolloComponents } from '@vue/apollo-components' + +app.use(VueApolloComponents) +``` + +## Components + +| Component | Wraps | Purpose | +|-----------|-------|---------| +| `` | `useQuery` | Fetch data, with `#loading` / `#error` / `#empty` / `#data` slots | +| `` | `useMutation` | Expose `mutate` to the template | +| `` | `useSubscription` | Stream results, renderless when given no slot | +| `` | *(nothing)* | Merge a subscription into the enclosing `` | +| `` | `useFragment` | Read a fragment from the cache, for data masking | + +Full prop, event and slot reference: + +## Notable differences from v4 + +- `disabled` replaces v4's `skip`, and inverts the composable's `enabled`. An absent prop + means the operation runs. +- `` has two modes. Providing `#data` opts into slot-per-state rendering; + using only the default slot hands you the raw state. The default slot renders in both. +- `#empty` is also the terminal branch: a query that settles with no result, no error and + no load in flight renders it. A `disabled` query renders nothing at all. +- Every prop maps to the option of the same name and keeps its `useQuery` default. Nothing + is toggled on for you based on which slots you pass. +- `` reads a single entity. Use `v-for` for lists, so each row gets its own + complete/incomplete branch. +- Components cannot suspend. Use `await useQuery(...)` in ` + + diff --git a/packages/vue-apollo-components/src/ApolloMutation.test.ts b/packages/vue-apollo-components/src/ApolloMutation.test.ts new file mode 100644 index 00000000..931f86b6 --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloMutation.test.ts @@ -0,0 +1,220 @@ +import type { Component } from 'vue' +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { h, ref } from 'vue' +import ApolloMutationGeneric from './ApolloMutation.vue' +import { ADD_THING, createMockClient, mockMutation } from './test-utils/client.ts' +import { provideClient, waitFor } from './test-utils/mount.ts' + +const ApolloMutation = ApolloMutationGeneric as unknown as Component + +/** A mutation whose variables miss every mock, so it always errors. */ +function mountFailing(props: Record = {}, settled: string[] = []) { + return mount(ApolloMutation, { + props: { + mutation: ADD_THING, + variables: { id: 'nope' }, + options: { errorPolicy: 'none' }, + ...props, + }, + slots: { + default: (slotProps: any) => + h('button', { + class: 'btn', + onClick: () => slotProps.mutate().then( + () => settled.push('resolved'), + () => settled.push('rejected'), + ), + }, 'go'), + }, + global: provideClient(createMockClient([mockMutation('x')])), + }) +} + +function mountMutation(props: Record = {}) { + return mount(ApolloMutation, { + props: { mutation: ADD_THING, variables: { id: 'x' }, ...props }, + slots: { + default: (slotProps: any) => + h('button', { + class: 'btn', + onClick: () => slotProps.mutate(), + }, `${slotProps.loading}|${slotProps.called}|${slotProps.error?.message ?? '-'}`), + }, + global: provideClient(createMockClient([mockMutation('x')])), + }) +} + +describe('apolloMutation', () => { + it('exposes mutate, loading and called, and emits done', async () => { + const wrapper = mountMutation() + + expect(wrapper.find('.btn').text()).toBe('false|false|-') + + await wrapper.find('.btn').trigger('click') + expect(wrapper.find('.btn').text()).toBe('true|true|-') + + await waitFor(() => wrapper.emitted('done') != null) + expect(wrapper.find('.btn').text()).toBe('false|true|-') + + const done = wrapper.emitted('done') as unknown[][] + expect((done[0]![0] as { data: { addThing: { id: string } } }).data.addThing.id).toBe('x') + + wrapper.unmount() + }) + + it('exposes result and reset on the slot', async () => { + const wrapper = mount(ApolloMutation, { + props: { mutation: ADD_THING, variables: { id: 'x' } }, + slots: { + default: (slotProps: any) => [ + h('button', { class: 'go', onClick: () => slotProps.mutate() }), + h('button', { class: 'reset', onClick: () => slotProps.reset() }), + h('span', { class: 'state' }, `${slotProps.result?.addThing?.id ?? '-'}|${slotProps.called}`), + ], + }, + global: provideClient(createMockClient([mockMutation('x')])), + }) + + expect(wrapper.find('.state').text()).toBe('-|false') + + await wrapper.find('.go').trigger('click') + await waitFor(() => wrapper.find('.state').text() === 'x|true') + + await wrapper.find('.reset').trigger('click') + await waitFor(() => wrapper.find('.state').text() === '-|false') + + wrapper.unmount() + }) + + it('emits error when the mutation fails', async () => { + const wrapper = mountFailing({ onError: () => {} }) + + await wrapper.find('.btn').trigger('click') + await waitFor(() => wrapper.emitted('error') != null) + + wrapper.unmount() + }) + + // The default `throws: 'auto'` rejects only while no `onError` handler is registered, so + // these pin that the `@error` bridge follows the parent rather than being unconditional. + it('rejects from mutate() when nothing is listening for @error', async () => { + const settled: string[] = [] + const wrapper = mountFailing({}, settled) + + await wrapper.find('.btn').trigger('click') + await waitFor(() => settled.length > 0) + + expect(settled).toEqual(['rejected']) + expect(wrapper.emitted('error')).toBeUndefined() + + wrapper.unmount() + }) + + it('resolves from mutate() when @error is bound', async () => { + const settled: string[] = [] + const wrapper = mountFailing({ onError: () => {} }, settled) + + await wrapper.find('.btn').trigger('click') + await waitFor(() => settled.length > 0) + + expect(settled).toEqual(['resolved']) + expect(wrapper.emitted('error')).toBeTruthy() + + wrapper.unmount() + }) + + it('picks up an @error listener the parent adds after mount', async () => { + const settled: string[] = [] + const listening = ref(false) + + const parent = mount({ + setup: () => () => h( + ApolloMutation, + { + mutation: ADD_THING, + variables: { id: 'nope' }, + options: { errorPolicy: 'none' }, + ...(listening.value ? { onError: () => {} } : {}), + }, + { + default: (slotProps: any) => h('button', { + class: 'btn', + onClick: () => slotProps.mutate().then( + () => settled.push('resolved'), + () => settled.push('rejected'), + ), + }, 'go'), + }, + ), + }, { global: provideClient(createMockClient([mockMutation('x')])) }) + + await parent.find('.btn').trigger('click') + await waitFor(() => settled.length > 0) + expect(settled).toEqual(['rejected']) + + listening.value = true + await parent.vm.$nextTick() + + await parent.find('.btn').trigger('click') + await waitFor(() => settled.length > 1) + expect(settled).toEqual(['rejected', 'resolved']) + + parent.unmount() + }) + + it('drops the @error listener when the parent stops listening', async () => { + const settled: string[] = [] + const listening = ref(true) + + const parent = mount({ + setup: () => () => h( + ApolloMutation, + { + mutation: ADD_THING, + variables: { id: 'nope' }, + options: { errorPolicy: 'none' }, + ...(listening.value ? { onError: () => {} } : {}), + }, + { + default: (slotProps: any) => h('button', { + class: 'btn', + onClick: () => slotProps.mutate().then( + () => settled.push('resolved'), + () => settled.push('rejected'), + ), + }, 'go'), + }, + ), + }, { global: provideClient(createMockClient([mockMutation('x')])) }) + + await parent.find('.btn').trigger('click') + await waitFor(() => settled.length > 0) + expect(settled).toEqual(['resolved']) + + listening.value = false + await parent.vm.$nextTick() + + // `throws: 'auto'` has to start rejecting again once nothing is listening. + await parent.find('.btn').trigger('click') + await waitFor(() => settled.length > 1) + expect(settled).toEqual(['resolved', 'rejected']) + + parent.unmount() + }) + + it('rejects when throws is set to always, even with @error bound', async () => { + const settled: string[] = [] + const wrapper = mountFailing( + { onError: () => {}, options: { errorPolicy: 'none', throws: 'always' } }, + settled, + ) + + await wrapper.find('.btn').trigger('click') + await waitFor(() => settled.length > 0) + + expect(settled).toEqual(['rejected']) + + wrapper.unmount() + }) +}) diff --git a/packages/vue-apollo-components/src/ApolloMutation.vue b/packages/vue-apollo-components/src/ApolloMutation.vue new file mode 100644 index 00000000..a8047339 --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloMutation.vue @@ -0,0 +1,94 @@ + + + diff --git a/packages/vue-apollo-components/src/ApolloQuery.test.ts b/packages/vue-apollo-components/src/ApolloQuery.test.ts new file mode 100644 index 00000000..fec2a11f --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloQuery.test.ts @@ -0,0 +1,518 @@ +import type { Component } from 'vue' +import type { ThingsData } from './test-utils/client.ts' +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { h } from 'vue' +import ApolloQueryGeneric from './ApolloQuery.vue' +import { captureWatchQuery, createMockClient, mock, THINGS_QUERY } from './test-utils/client.ts' +import { provideClient, provideClients, waitFor } from './test-utils/mount.ts' + +// `mount()` cannot infer generic components; slot typing is covered by type-tests/. +const ApolloQuery = ApolloQueryGeneric as unknown as Component + +function ids(data: ThingsData) { + return data.things.map(thing => thing.id).join(',') +} + +function mountQuery( + client: ReturnType, + slots: Record any>, + props: Record = {}, +) { + return mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, ...props }, + slots, + global: provideClient(client), + }) +} + +function dataSlot(props: any) { + return h('div', { class: 'data' }, `${ids(props.data)}|${props.isPreviousResult}`) +} + +const loadingSlot = () => h('div', { class: 'loading' }, 'loading') + +describe('apolloQuery opinionated mode', () => { + it('renders #loading, then #data', async () => { + const wrapper = mountQuery(createMockClient([mock('a', ['a1'])]), { + loading: loadingSlot, + data: dataSlot, + }) + + expect(wrapper.find('.loading').exists()).toBe(true) + + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.data').text()).toBe('a1|false') + + wrapper.unmount() + }) + + it('clears the result on a variables change, with no `keep-previous-result` prop', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' } }, + slots: { loading: loadingSlot, data: dataSlot }, + global: provideClient(createMockClient([mock('a', ['a1']), mock('b', ['b1'])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + + await wrapper.setProps({ variables: { term: 'b' } }) + + // Providing #data must not opt into retention on the user's behalf. + expect(wrapper.find('.data').exists()).toBe(false) + expect(wrapper.find('.loading').exists()).toBe(true) + + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.data').text()).toBe('b1|false') + + wrapper.unmount() + }) + + it('keeps the previous result when `keep-previous-result` is set', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, keepPreviousResult: true }, + slots: { loading: loadingSlot, data: dataSlot }, + global: provideClient(createMockClient([mock('a', ['a1']), mock('b', ['b1'])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.data').text()).toBe('a1|false') + + await wrapper.setProps({ variables: { term: 'b' } }) + + // Retained rows stay on screen, flagged as previous, so no skeleton flash. + expect(wrapper.find('.loading').exists()).toBe(false) + expect(wrapper.find('.data').text()).toBe('a1|true') + + await waitFor(() => wrapper.find('.data').text() === 'b1|false') + + wrapper.unmount() + }) + + // A failed refetch leaves the rows on screen, so `#error` never runs and the failure + // would be invisible inside the slot without its own `error` prop. + it('passes the error to #data when a refetch fails over existing rows', async () => { + const client = createMockClient([ + { ...mock('a', ['a1']), maxUsageCount: 1 }, + { ...mock('a', [], { error: new Error('boom') }), maxUsageCount: 1 }, + ]) + + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' } }, + slots: { + loading: loadingSlot, + error: () => h('div', { class: 'error-slot' }, 'error slot'), + data: (props: any) => + h('div', { class: 'data' }, `${ids(props.data)}|${props.error?.message ?? '-'}`), + }, + global: provideClient(client), + }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.data').text()).toBe('a1|-') + + const apollo = wrapper.vm as unknown as { refetch: () => Promise } + await apollo.refetch().catch(() => {}) + await waitFor(() => wrapper.find('.data').text() !== 'a1|-') + + expect(wrapper.find('.error-slot').exists()).toBe(false) + expect(wrapper.find('.data').text()).toBe('a1|boom') + + wrapper.unmount() + }) + + it('renders #empty when the predicate matches', async () => { + const wrapper = mountQuery( + createMockClient([mock('a', [])]), + { loading: loadingSlot, empty: () => h('div', { class: 'empty' }, 'empty'), data: dataSlot }, + { empty: (data: ThingsData) => data.things.length === 0 }, + ) + + await waitFor(() => wrapper.find('.empty').exists()) + expect(wrapper.find('.data').exists()).toBe(false) + + wrapper.unmount() + }) + + it('renders #error when the query fails with nothing to show', async () => { + const wrapper = mountQuery( + createMockClient([mock('a', [], { error: new Error('boom') })]), + { + loading: loadingSlot, + error: (props: any) => h('div', { class: 'error' }, props.error.message), + data: dataSlot, + }, + ) + + await waitFor(() => wrapper.find('.error').exists()) + expect(wrapper.find('.error').text()).toContain('boom') + + wrapper.unmount() + }) +}) + +describe('apolloQuery raw mode', () => { + it('passes flattened state to #default and does not retain by default', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' } }, + slots: { + default: (props: any) => + h('div', { class: 'raw' }, `${props.resultState}|${props.loading}|${typeof props.refetch}`), + }, + global: provideClient(createMockClient([mock('a', ['a1']), mock('b', ['b1'])])), + }) + + expect(wrapper.find('.raw').text()).toBe('empty|true|function') + + await waitFor(() => wrapper.find('.raw').text() === 'complete|false|function') + + await wrapper.setProps({ variables: { term: 'b' } }) + expect(wrapper.find('.raw').text()).toBe('empty|true|function') + + wrapper.unmount() + }) +}) + +describe('apolloQuery events', () => { + it('emits result, complete-result and next-state', async () => { + const wrapper = mountQuery(createMockClient([mock('a', ['a1'])]), { data: dataSlot }) + + await waitFor(() => wrapper.find('.data').exists()) + + const result = wrapper.emitted('result') + expect(result).toHaveLength(1) + expect(ids((result as unknown[][])[0]![0] as ThingsData)).toBe('a1') + expect(wrapper.emitted('completeResult')).toHaveLength(1) + expect(wrapper.emitted('nextState')?.length ?? 0).toBeGreaterThan(0) + + wrapper.unmount() + }) + + it('emits error', async () => { + const wrapper = mountQuery( + createMockClient([mock('a', [], { error: new Error('boom') })]), + { data: dataSlot, error: () => h('div', { class: 'error' }, 'error') }, + ) + + await waitFor(() => wrapper.find('.error').exists()) + expect(wrapper.emitted('error')).toHaveLength(1) + + wrapper.unmount() + }) +}) + +describe('apolloQuery disabled', () => { + it('runs by default, with no `disabled` prop given', async () => { + const client = createMockClient([mock('a', ['a1'])]) + const calls = captureWatchQuery(client) + const wrapper = mountQuery(client, { data: dataSlot }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(calls).toHaveLength(1) + + wrapper.unmount() + }) + + it('does not execute while disabled, then runs once re-enabled', async () => { + const client = createMockClient([mock('a', ['a1'])]) + const calls = captureWatchQuery(client) + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, disabled: true }, + slots: { data: dataSlot, loading: loadingSlot }, + global: provideClient(client), + }) + + await new Promise(resolve => setTimeout(resolve, 40)) + expect(calls).toHaveLength(0) + expect(wrapper.find('.data').exists()).toBe(false) + + await wrapper.setProps({ disabled: false }) + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.data').text()).toBe('a1|false') + + wrapper.unmount() + }) + + it('leaves `options.enabled` alone when `disabled` is not set', () => { + const client = createMockClient([mock('a', ['a1'])]) + const calls = captureWatchQuery(client) + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, options: { enabled: false } }, + slots: { data: dataSlot }, + global: provideClient(client), + }) + + expect(calls).toHaveLength(0) + + wrapper.unmount() + }) +}) + +describe('apolloQuery options passthrough', () => { + it('applies `options` entries that have no dedicated prop', () => { + const client = createMockClient([mock('a', ['a1'])]) + const calls = captureWatchQuery(client) + + const wrapper = mount(ApolloQuery, { + props: { + query: THINGS_QUERY, + variables: { term: 'a' }, + options: { errorPolicy: 'all' }, + }, + slots: { data: dataSlot }, + global: provideClient(client), + }) + + expect(calls[0]).toMatchObject({ errorPolicy: 'all' }) + + wrapper.unmount() + }) + + it('does not let an absent prop overwrite the same key in `options`', () => { + const client = createMockClient([mock('a', ['a1'])]) + const calls = captureWatchQuery(client) + + const wrapper = mount(ApolloQuery, { + props: { + query: THINGS_QUERY, + variables: { term: 'a' }, + // Each of these also exists as a dedicated prop, left unset here. + options: { fetchPolicy: 'no-cache', pollInterval: 1234 }, + }, + slots: { data: dataSlot }, + global: provideClient(client), + }) + + expect(calls[0]).toMatchObject({ fetchPolicy: 'no-cache', pollInterval: 1234 }) + + wrapper.unmount() + }) + + it('does not let an absent boolean prop overwrite the same key in `options`', async () => { + const wrapper = mount(ApolloQuery, { + props: { + query: THINGS_QUERY, + variables: { term: 'a' }, + // Absent boolean props are cast to `false` unless the component declares a default. + options: { keepPreviousResult: true }, + }, + slots: { loading: loadingSlot, data: dataSlot }, + global: provideClient(createMockClient([mock('a', ['a1']), mock('b', ['b1'])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + await wrapper.setProps({ variables: { term: 'b' } }) + + expect(wrapper.find('.data').text()).toBe('a1|true') + + wrapper.unmount() + }) + + it('lets a dedicated prop win over `options`', () => { + const client = createMockClient([mock('a', ['a1'])]) + const calls = captureWatchQuery(client) + + const wrapper = mount(ApolloQuery, { + props: { + query: THINGS_QUERY, + variables: { term: 'a' }, + fetchPolicy: 'cache-only', + options: { fetchPolicy: 'no-cache' }, + }, + slots: { data: dataSlot }, + global: provideClient(client), + }) + + expect(calls[0]).toMatchObject({ fetchPolicy: 'cache-only' }) + + wrapper.unmount() + }) +}) + +describe('apolloQuery empty predicate', () => { + it('ignores #empty when no `empty` prop is given, so #data owns the empty case', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' } }, + slots: { data: dataSlot, empty: () => h('div', { class: 'empty' }, 'empty') }, + global: provideClient(createMockClient([mock('a', [])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.empty').exists()).toBe(false) + + wrapper.unmount() + }) + + it('falls back to #data when `empty` is given without an #empty slot', async () => { + const wrapper = mount(ApolloQuery, { + props: { + query: THINGS_QUERY, + variables: { term: 'a' }, + empty: (data: ThingsData) => data.things.length === 0, + }, + slots: { data: dataSlot }, + global: provideClient(createMockClient([mock('a', [])])), + }) + + // Without the fallback this renders nothing at all. + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.data').text()).toBe('|false') + + wrapper.unmount() + }) + + it('renders #empty when the query settles with nothing to show', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, fetchPolicy: 'cache-only' }, + slots: { + data: dataSlot, + loading: loadingSlot, + error: () => h('div', { class: 'error' }, 'error'), + empty: () => h('div', { class: 'empty' }, 'empty'), + }, + global: provideClient(createMockClient([])), + }) + + // A cold `cache-only` query is not loading, has no error and has no result. + await waitFor(() => wrapper.find('.empty').exists()) + + wrapper.unmount() + }) + + it('renders nothing at all while disabled, even with an #empty slot', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, disabled: true }, + slots: { data: dataSlot, loading: loadingSlot, empty: () => h('div', { class: 'empty' }, 'empty') }, + global: provideClient(createMockClient([mock('a', ['a1'])])), + }) + + await new Promise(resolve => setTimeout(resolve, 40)) + expect(wrapper.text()).toBe('') + + wrapper.unmount() + }) +}) + +describe('apolloQuery cache hits', () => { + /** `useQuery` applies a cached result during setup, before the event bridges exist. */ + it('emits for a result that was already in the cache', async () => { + const client = createMockClient([mock('a', ['a1'])]) + const warm = mountQuery(client, { data: dataSlot }) + await waitFor(() => warm.find('.data').exists()) + warm.unmount() + + const wrapper = mountQuery(client, { data: dataSlot }, { fetchPolicy: 'cache-only' }) + await wrapper.vm.$nextTick() + + expect(wrapper.find('.data').text()).toBe('a1|false') + expect(wrapper.emitted('result')).toHaveLength(1) + expect(wrapper.emitted('completeResult')).toHaveLength(1) + expect(wrapper.emitted('nextState')).toHaveLength(1) + + wrapper.unmount() + }) + + it('emits exactly once when the result arrives from the network', async () => { + const wrapper = mountQuery(createMockClient([mock('a', ['a1'])]), { data: dataSlot }) + + await waitFor(() => wrapper.find('.data').exists()) + + expect(wrapper.emitted('result')).toHaveLength(1) + expect(wrapper.emitted('completeResult')).toHaveLength(1) + + wrapper.unmount() + }) +}) + +describe('apolloQuery exposed instance', () => { + // `useQuery` returns a `PromiseLike`; leaving `then` on the instance would make + // `await apolloRef.value` resolve to something other than the component. + it('is not a thenable', async () => { + let instance: any + const wrapper = mount({ + setup: () => () => h( + ApolloQuery, + { query: THINGS_QUERY, variables: { term: 'a' }, ref: (value: any) => { instance = value } }, + { data: dataSlot }, + ), + }, { global: provideClient(createMockClient([mock('a', ['a1'])])) }) + + await wrapper.vm.$nextTick() + expect(instance.then).toBeUndefined() + + wrapper.unmount() + }) +}) + +describe('apolloQuery variable timing', () => { + /** `.text()` throws on a missing element, and both slots blank out mid-transition. */ + function dataText(wrapper: ReturnType) { + const found = wrapper.find('.data') + return found.exists() ? found.text() : '' + } + + it('delays a variables change by `debounce`, covering the window with `loading`', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, debounce: 150 }, + slots: { + data: (props: any) => h('div', { class: 'data' }, `${ids(props.data)}|${props.loading}|${props.pending}`), + }, + global: provideClient(createMockClient([mock('a', ['a1']), mock('b', ['b1'])])), + }) + + await waitFor(() => dataText(wrapper) === 'a1|false|false') + + await wrapper.setProps({ variables: { term: 'b' } }) + await new Promise(resolve => setTimeout(resolve, 40)) + + // Still the old rows, and the wait is reported rather than hidden. + expect(dataText(wrapper)).toBe('a1|true|true') + + await waitFor(() => dataText(wrapper) === 'b1|false|false') + + wrapper.unmount() + }) + + it('coalesces rapid variable changes under `throttle`', async () => { + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, throttle: 150 }, + slots: { data: dataSlot }, + global: provideClient(createMockClient([mock('a', ['a1']), mock('b', ['b1']), mock('c', ['c1'])])), + }) + + await waitFor(() => dataText(wrapper) === 'a1|false') + + await wrapper.setProps({ variables: { term: 'b' } }) + await wrapper.setProps({ variables: { term: 'c' } }) + await new Promise(resolve => setTimeout(resolve, 40)) + + // The trailing value waits out the window rather than firing a third request. + expect(dataText(wrapper)).not.toBe('c1|false') + + await waitFor(() => dataText(wrapper) === 'c1|false') + + wrapper.unmount() + }) +}) + +describe('apolloQuery clientId', () => { + it('resolves against the provided ApolloClients map', async () => { + const analytics = createMockClient([mock('a', ['a1'])]) + const fallback = createMockClient([mock('a', ['nope'])]) + const analyticsCalls = captureWatchQuery(analytics) + const fallbackCalls = captureWatchQuery(fallback) + + const wrapper = mount(ApolloQuery, { + props: { query: THINGS_QUERY, variables: { term: 'a' }, clientId: 'analytics' }, + slots: { data: dataSlot }, + global: provideClients({ analytics, default: fallback }), + }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(wrapper.find('.data').text()).toBe('a1|false') + expect(analyticsCalls).toHaveLength(1) + expect(fallbackCalls).toHaveLength(0) + + wrapper.unmount() + }) +}) diff --git a/packages/vue-apollo-components/src/ApolloQuery.vue b/packages/vue-apollo-components/src/ApolloQuery.vue new file mode 100644 index 00000000..7aebbbaa --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloQuery.vue @@ -0,0 +1,210 @@ + + + diff --git a/packages/vue-apollo-components/src/ApolloSubscribeToMore.test.ts b/packages/vue-apollo-components/src/ApolloSubscribeToMore.test.ts new file mode 100644 index 00000000..082a206d --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloSubscribeToMore.test.ts @@ -0,0 +1,275 @@ +import type { ErrorLike } from '@apollo/client' +import type { Component } from 'vue' +import { ObservableQuery } from '@apollo/client' +import { mount } from '@vue/test-utils' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { defineComponent, h, shallowRef } from 'vue' +import ApolloQueryGeneric from './ApolloQuery.vue' +import ApolloSubscribeToMoreGeneric from './ApolloSubscribeToMore.vue' +import { createMockClient, mock, THING_ADDED, THINGS_QUERY } from './test-utils/client.ts' +import { provideClient, waitFor } from './test-utils/mount.ts' + +const ApolloQuery = ApolloQueryGeneric as unknown as Component +const ApolloSubscribeToMore = ApolloSubscribeToMoreGeneric as unknown as Component + +afterEach(() => { + vi.restoreAllMocks() +}) + +/** Subscriber as default-slot content, with `variables` rebuilt on every render. */ +function createParent(updateQuery = (previous: unknown) => previous) { + return defineComponent({ + props: { tick: { type: Number, default: 0 } }, + setup(props) { + return () => + h(ApolloQuery, { query: THINGS_QUERY, variables: { term: 'a' } }, { + data: (slotProps: any) => + h('div', { class: 'data' }, `${slotProps.data.things.length}|${props.tick}`), + default: () => + h(ApolloSubscribeToMore, { + document: THING_ADDED, + variables: { term: 'a' }, + updateQuery, + }), + }) + }, + }) +} + +describe('apolloSubscribeToMore', () => { + it('mounts as default-slot content alongside a #data slot', async () => { + const subscribeToMore = vi.spyOn(ObservableQuery.prototype, 'subscribeToMore') + const wrapper = mount(createParent(), { + global: provideClient(createMockClient([mock('a', ['a1'])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + + // Opinionated mode must still render default-slot children, or nothing subscribes. + expect(subscribeToMore).toHaveBeenCalledTimes(1) + expect(subscribeToMore.mock.calls[0]![0]).toMatchObject({ + document: THING_ADDED, + variables: { term: 'a' }, + }) + + wrapper.unmount() + }) + + it('does not re-subscribe when the parent re-renders with equal variables', async () => { + const subscribeToMore = vi.spyOn(ObservableQuery.prototype, 'subscribeToMore') + const wrapper = mount(createParent(), { + global: provideClient(createMockClient([mock('a', ['a1'])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(subscribeToMore).toHaveBeenCalledTimes(1) + + // Fresh `variables` identity each render; comparing by identity would re-subscribe. + await wrapper.setProps({ tick: 1 }) + await wrapper.setProps({ tick: 2 }) + await waitFor(() => wrapper.find('.data').text().endsWith('|2')) + + expect(subscribeToMore).toHaveBeenCalledTimes(1) + + wrapper.unmount() + }) + + it('leaves the subscription open across equal re-renders', async () => { + // Counting `subscribeToMore` calls is not enough: a cleanup that fires on every + // watcher re-run tears the subscription down without the count ever changing. + let unsubscribed = 0 + const original = ObservableQuery.prototype.subscribeToMore + vi.spyOn(ObservableQuery.prototype, 'subscribeToMore').mockImplementation( + function (this: ObservableQuery, options: Parameters[0]) { + const off = original.call(this, options) + return () => { + unsubscribed++ + return off?.() + } + }, + ) + + const wrapper = mount(createParent(), { + global: provideClient(createMockClient([mock('a', ['a1'])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(unsubscribed).toBe(0) + + await wrapper.setProps({ tick: 1 }) + await wrapper.setProps({ tick: 2 }) + await waitFor(() => wrapper.find('.data').text().endsWith('|2')) + + expect(unsubscribed).toBe(0) + + wrapper.unmount() + expect(unsubscribed).toBe(1) + }) + + it('re-subscribes when the variables actually change', async () => { + const subscribeToMore = vi.spyOn(ObservableQuery.prototype, 'subscribeToMore') + const Parent = defineComponent({ + props: { term: { type: String, default: 'a' } }, + setup(props) { + return () => + h(ApolloQuery, { query: THINGS_QUERY, variables: { term: 'a' } }, { + data: () => h('div', { class: 'data' }, 'data'), + default: () => + h(ApolloSubscribeToMore, { document: THING_ADDED, variables: { term: props.term } }), + }) + }, + }) + + const wrapper = mount(Parent, { global: provideClient(createMockClient([mock('a', ['a1'])])) }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(subscribeToMore).toHaveBeenCalledTimes(1) + + await wrapper.setProps({ term: 'b' }) + expect(subscribeToMore).toHaveBeenCalledTimes(2) + expect(subscribeToMore.mock.calls[1]![0]).toMatchObject({ variables: { term: 'b' } }) + + wrapper.unmount() + }) + + it('calls through to the latest updateQuery prop without re-subscribing', async () => { + const subscribeToMore = vi.spyOn(ObservableQuery.prototype, 'subscribeToMore') + const first = vi.fn((previous: unknown) => previous) + const second = vi.fn((previous: unknown) => previous) + const updateQuery = shallowRef(first) + + const Parent = defineComponent({ + setup: () => () => + h(ApolloQuery, { query: THINGS_QUERY, variables: { term: 'a' } }, { + data: () => h('div', { class: 'data' }, 'data'), + default: () => + h(ApolloSubscribeToMore, { + document: THING_ADDED, + variables: { term: 'a' }, + updateQuery: updateQuery.value, + }), + }), + }) + + const wrapper = mount(Parent, { global: provideClient(createMockClient([mock('a', ['a1'])])) }) + + await waitFor(() => wrapper.find('.data').exists()) + + updateQuery.value = second + await wrapper.vm.$nextTick() + + const options = subscribeToMore.mock.calls[0]![0] as { + updateQuery?: (previous: unknown, options: unknown) => unknown + } + options.updateQuery?.({ things: [] }, {}) + + // The swap is picked up without tearing the subscription down. + expect(second).toHaveBeenCalledTimes(1) + expect(first).not.toHaveBeenCalled() + expect(subscribeToMore).toHaveBeenCalledTimes(1) + + wrapper.unmount() + }) + + it('emits error when the subscription fails', async () => { + const subscribeToMore = vi.spyOn(ObservableQuery.prototype, 'subscribeToMore') + const onError = vi.fn() + const Parent = defineComponent({ + setup: () => () => + h(ApolloQuery, { query: THINGS_QUERY, variables: { term: 'a' } }, { + data: () => h('div', { class: 'data' }, 'data'), + default: () => + h(ApolloSubscribeToMore, { document: THING_ADDED, variables: { term: 'a' }, onError }), + }), + }) + + const wrapper = mount(Parent, { global: provideClient(createMockClient([mock('a', ['a1'])])) }) + + await waitFor(() => wrapper.find('.data').exists()) + + const options = subscribeToMore.mock.calls[0]![0] as { onError?: (error: ErrorLike) => void } + const failure = new Error('socket closed') + options.onError?.(failure) + await wrapper.vm.$nextTick() + + expect(onError).toHaveBeenCalledWith(failure) + + wrapper.unmount() + }) + + it('forwards the context prop', async () => { + const subscribeToMore = vi.spyOn(ObservableQuery.prototype, 'subscribeToMore') + const Parent = defineComponent({ + setup: () => () => + h(ApolloQuery, { query: THINGS_QUERY, variables: { term: 'a' } }, { + data: () => h('div', { class: 'data' }, 'data'), + default: () => + h(ApolloSubscribeToMore, { + document: THING_ADDED, + variables: { term: 'a' }, + context: { headers: { authorization: 'token' } }, + }), + }), + }) + + const wrapper = mount(Parent, { global: provideClient(createMockClient([mock('a', ['a1'])])) }) + + await waitFor(() => wrapper.find('.data').exists()) + expect(subscribeToMore.mock.calls[0]![0]).toMatchObject({ + context: { headers: { authorization: 'token' } }, + }) + + wrapper.unmount() + }) + + /* + * `useQuery` destroys and recreates its ObservableQuery on every enabled flip, taking its + * subscriptions with it, so keying only on the document and variables never re-subscribes. + */ + it('re-subscribes when the parent query is re-enabled', async () => { + const subscribeToMore = vi.spyOn(ObservableQuery.prototype, 'subscribeToMore') + const disabled = shallowRef(true) + const Parent = defineComponent({ + setup: () => () => + h(ApolloQuery, { query: THINGS_QUERY, variables: { term: 'a' }, disabled: disabled.value }, { + data: () => h('div', { class: 'data' }, 'data'), + default: () => + h(ApolloSubscribeToMore, { document: THING_ADDED, variables: { term: 'a' } }), + }), + }) + + const wrapper = mount(Parent, { global: provideClient(createMockClient([mock('a', ['a1'])])) }) + + await wrapper.vm.$nextTick() + expect(subscribeToMore).toHaveBeenCalledTimes(0) + + disabled.value = false + await waitFor(() => wrapper.find('.data').exists()) + + expect(subscribeToMore).toHaveBeenCalledTimes(1) + + wrapper.unmount() + }) + + it('renders a comment node and nothing else', async () => { + const wrapper = mount(createParent(), { + global: provideClient(createMockClient([mock('a', ['a1'])])), + }) + + await waitFor(() => wrapper.find('.data').exists()) + + // An empty template renders no node at all, which breaks a keyed sibling list. + expect(wrapper.findComponent(ApolloSubscribeToMore).element.nodeType).toBe(Node.COMMENT_NODE) + + wrapper.unmount() + }) + + it('throws when used outside an ApolloQuery', () => { + expect(() => + mount(ApolloSubscribeToMore, { + props: { document: THING_ADDED, variables: { term: 'a' } }, + global: provideClient(createMockClient([])), + }), + ).toThrow(/must be nested inside an /) + }) +}) diff --git a/packages/vue-apollo-components/src/ApolloSubscribeToMore.vue b/packages/vue-apollo-components/src/ApolloSubscribeToMore.vue new file mode 100644 index 00000000..a48b5edc --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloSubscribeToMore.vue @@ -0,0 +1,69 @@ + + + diff --git a/packages/vue-apollo-components/src/ApolloSubscription.test.ts b/packages/vue-apollo-components/src/ApolloSubscription.test.ts new file mode 100644 index 00000000..a8945c88 --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloSubscription.test.ts @@ -0,0 +1,151 @@ +import type { Component } from 'vue' +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { h, nextTick } from 'vue' +import ApolloSubscriptionGeneric from './ApolloSubscription.vue' +import { createSubscriptionClient, THING_ADDED } from './test-utils/client.ts' +import { provideClient, waitFor } from './test-utils/mount.ts' + +const ApolloSubscription = ApolloSubscriptionGeneric as unknown as Component + +const thing = { result: { data: { thingAdded: { __typename: 'Thing', id: 'a1' } } } } + +describe('apolloSubscription', () => { + it('renders nothing and emits result when no slot is given', async () => { + const { client, link } = createSubscriptionClient() + const wrapper = mount(ApolloSubscription, { + props: { subscription: THING_ADDED, variables: { term: 'a' } }, + global: provideClient(client), + }) + + // The usual usage: a handler component with no output of its own. + expect(wrapper.html()).toBe('') + + await nextTick() + link.simulateResult(thing) + + await waitFor(() => wrapper.emitted('result') != null) + const emitted = wrapper.emitted('result') as unknown[][] + expect((emitted[0]![0] as { thingAdded: { id: string } }).thingAdded.id).toBe('a1') + + wrapper.unmount() + }) + + it('renders the default slot when given one', async () => { + const { client, link } = createSubscriptionClient() + const wrapper = mount(ApolloSubscription, { + props: { subscription: THING_ADDED, variables: { term: 'a' } }, + slots: { + default: (slotProps: any) => + h('div', { class: 'sub' }, slotProps.result?.thingAdded.id ?? 'none'), + }, + global: provideClient(client), + }) + + expect(wrapper.find('.sub').text()).toBe('none') + + await nextTick() + link.simulateResult(thing) + + await waitFor(() => wrapper.find('.sub').text() === 'a1') + + wrapper.unmount() + }) + + it('emits complete when the subscription closes', async () => { + const { client, link } = createSubscriptionClient() + const wrapper = mount(ApolloSubscription, { + props: { subscription: THING_ADDED, variables: { term: 'a' } }, + global: provideClient(client), + }) + + await nextTick() + link.simulateResult(thing, true) + + await waitFor(() => wrapper.emitted('complete') != null) + + wrapper.unmount() + }) + + it('does not subscribe while disabled, then subscribes once re-enabled', async () => { + const { client, link } = createSubscriptionClient() + const wrapper = mount(ApolloSubscription, { + props: { subscription: THING_ADDED, variables: { term: 'a' }, disabled: true }, + global: provideClient(client), + }) + + await nextTick() + expect(link.operation).toBeUndefined() + expect(wrapper.emitted('result')).toBeUndefined() + + await wrapper.setProps({ disabled: false }) + await nextTick() + expect(link.operation).toBeDefined() + + wrapper.unmount() + }) + + it('subscribes by default, with no `disabled` prop given', async () => { + const { client, link } = createSubscriptionClient() + const wrapper = mount(ApolloSubscription, { + props: { subscription: THING_ADDED, variables: { term: 'a' } }, + global: provideClient(client), + }) + + await nextTick() + expect(link.operation).toBeDefined() + + wrapper.unmount() + }) + + it('leaves `options.enabled` alone when `disabled` is not set', async () => { + const { client, link } = createSubscriptionClient() + const wrapper = mount(ApolloSubscription, { + props: { subscription: THING_ADDED, variables: { term: 'a' }, options: { enabled: false } }, + global: provideClient(client), + }) + + await nextTick() + expect(link.operation).toBeUndefined() + + wrapper.unmount() + }) + + // The whole lifecycle is on the slot, so pausing needs no template ref. + it('drives the lifecycle from the default slot', async () => { + const { client, link } = createSubscriptionClient() + let subscribes = 0 + let unsubscribes = 0 + link.onSetup(() => { + subscribes++ + }) + link.onUnsubscribe(() => { + unsubscribes++ + }) + + const wrapper = mount(ApolloSubscription, { + props: { subscription: THING_ADDED, variables: { term: 'a' } }, + slots: { + default: (slotProps: any) => [ + h('button', { class: 'stop', onClick: () => slotProps.stop() }), + h('button', { class: 'start', onClick: () => slotProps.start() }), + h('button', { class: 'restart', onClick: () => slotProps.restart() }), + ], + }, + global: provideClient(client), + }) + + await waitFor(() => subscribes === 1) + + await wrapper.find('.stop').trigger('click') + await waitFor(() => unsubscribes === 1) + + await wrapper.find('.start').trigger('click') + await waitFor(() => subscribes === 2) + + await wrapper.find('.restart').trigger('click') + await waitFor(() => subscribes === 3) + + wrapper.unmount() + }) +}) diff --git a/packages/vue-apollo-components/src/ApolloSubscription.vue b/packages/vue-apollo-components/src/ApolloSubscription.vue new file mode 100644 index 00000000..804d3e2f --- /dev/null +++ b/packages/vue-apollo-components/src/ApolloSubscription.vue @@ -0,0 +1,83 @@ + + + diff --git a/packages/vue-apollo-components/src/index.ts b/packages/vue-apollo-components/src/index.ts new file mode 100644 index 00000000..4331f433 --- /dev/null +++ b/packages/vue-apollo-components/src/index.ts @@ -0,0 +1,31 @@ +import type { App, Plugin } from 'vue' +import ApolloFragment from './ApolloFragment.vue' +import ApolloMutation from './ApolloMutation.vue' +import ApolloQuery from './ApolloQuery.vue' +import ApolloSubscribeToMore from './ApolloSubscribeToMore.vue' +import ApolloSubscription from './ApolloSubscription.vue' + +export { ApolloFragment, ApolloMutation, ApolloQuery, ApolloSubscribeToMore, ApolloSubscription } +export { ApolloQueryKey } from './keys.ts' + +/** + * Registers every component globally. + * + * Prefer importing them directly: global registration loses the generic slot-prop types. + * + * @example + * ```ts + * app.use(VueApolloComponents) + * ``` + */ +export const VueApolloComponents: Plugin = { + install(app: App) { + app.component('ApolloQuery', ApolloQuery) + app.component('ApolloFragment', ApolloFragment) + app.component('ApolloMutation', ApolloMutation) + app.component('ApolloSubscription', ApolloSubscription) + app.component('ApolloSubscribeToMore', ApolloSubscribeToMore) + }, +} + +export default VueApolloComponents diff --git a/packages/vue-apollo-components/src/keys.ts b/packages/vue-apollo-components/src/keys.ts new file mode 100644 index 00000000..420e1558 --- /dev/null +++ b/packages/vue-apollo-components/src/keys.ts @@ -0,0 +1,7 @@ +import type { OperationVariables } from '@apollo/client' +import type { useQuery } from '@vue/apollo-composable' +import type { InjectionKey } from 'vue' + +/** Provided by `ApolloQuery` so a nested `ApolloSubscribeToMore` can extend it. */ +export const ApolloQueryKey: InjectionKey> + = Symbol('apollo-query') diff --git a/packages/vue-apollo-components/src/test-utils/client.ts b/packages/vue-apollo-components/src/test-utils/client.ts new file mode 100644 index 00000000..6cb2c2ec --- /dev/null +++ b/packages/vue-apollo-components/src/test-utils/client.ts @@ -0,0 +1,98 @@ +import type { ApolloClient as ApolloClientType, TypedDocumentNode } from '@apollo/client' +import { ApolloClient, gql, InMemoryCache } from '@apollo/client' +import { MockLink, MockSubscriptionLink } from '@apollo/client/testing' + +export interface Thing { __typename: 'Thing', id: string } +export interface ThingsData { things: Thing[] } +export interface ThingsVars { term: string } + +export const THINGS_QUERY = gql` + query Things($term: String!) { + things(term: $term) { + id + } + } +` as TypedDocumentNode + +export const ADD_THING = gql` + mutation AddThing($id: String!) { + addThing(id: $id) { + id + } + } +` as TypedDocumentNode<{ addThing: Thing }, { id: string }> + +export const THING_ADDED = gql` + subscription ThingAdded($term: String!) { + thingAdded(term: $term) { + id + } + } +` as TypedDocumentNode<{ thingAdded: Thing }, ThingsVars> + +export function mock(term: string, ids: string[], options: { delay?: number, error?: Error } = {}) { + return { + request: { query: THINGS_QUERY, variables: { term } }, + maxUsageCount: Number.POSITIVE_INFINITY, + delay: options.delay ?? 20, + ...(options.error + ? { error: options.error } + : { result: { data: { things: ids.map(id => ({ __typename: 'Thing', id })) } } }), + } +} + +export const THING_FRAGMENT = gql` + fragment ThingFields on Thing { + id + label + } +` as TypedDocumentNode<{ id: string, label: string }, Record> + +export function mockMutation(id: string) { + return { + request: { query: ADD_THING, variables: { id } }, + maxUsageCount: Number.POSITIVE_INFINITY, + delay: 20, + result: { data: { addThing: { __typename: 'Thing', id } } }, + } +} + +export function createMockClient(mocks: readonly unknown[]) { + return new ApolloClient({ + link: new MockLink(mocks as never, { showWarnings: false }), + cache: new InMemoryCache(), + }) +} + +/** Client whose subscriptions are driven by the returned link. */ +export function createSubscriptionClient() { + const link = new MockSubscriptionLink() + const client = new ApolloClient({ link, cache: new InMemoryCache() }) + return { client, link } +} + +/** Records the options each `watchQuery` actually receives. */ +export function captureWatchQuery(client: ApolloClientType) { + const calls: Record[] = [] + const original = client.watchQuery.bind(client) + + client.watchQuery = ((options: never) => { + calls.push(options) + return original(options) + }) as typeof client.watchQuery + + return calls +} + +/** Records the options each `watchFragment` actually receives. */ +export function captureWatchFragment(client: ApolloClientType) { + const calls: Record[] = [] + const original = client.watchFragment.bind(client) + + client.watchFragment = ((options: never) => { + calls.push(options) + return original(options) + }) as typeof client.watchFragment + + return calls +} diff --git a/packages/vue-apollo-components/src/test-utils/mount.ts b/packages/vue-apollo-components/src/test-utils/mount.ts new file mode 100644 index 00000000..2598be26 --- /dev/null +++ b/packages/vue-apollo-components/src/test-utils/mount.ts @@ -0,0 +1,29 @@ +import type { ApolloClient } from '@apollo/client' +import { ApolloClients, DefaultApolloClient } from '@vue/apollo-composable' +import { nextTick } from 'vue' + +/** `until()` cannot watch a DOM query, so poll. */ +export async function waitFor(predicate: () => boolean, timeout = 2000) { + const deadline = Date.now() + timeout + while (!predicate()) { + if (Date.now() > deadline) { + throw new Error('waitFor timed out') + } + await new Promise(resolve => setTimeout(resolve, 5)) + await nextTick() + } +} + +export function provideClient(client: ApolloClient) { + return { provide: { [DefaultApolloClient as symbol]: client } } +} + +/** For `clientId`, which resolves against the `ApolloClients` map rather than the default. */ +export function provideClients(clients: Record, defaultClient?: ApolloClient) { + return { + provide: { + [ApolloClients as symbol]: clients, + ...(defaultClient == null ? {} : { [DefaultApolloClient as symbol]: defaultClient }), + }, + } +} diff --git a/packages/vue-apollo-components/src/type-tests/consumer.vue b/packages/vue-apollo-components/src/type-tests/consumer.vue new file mode 100644 index 00000000..4fc9ac71 --- /dev/null +++ b/packages/vue-apollo-components/src/type-tests/consumer.vue @@ -0,0 +1,81 @@ + + + diff --git a/packages/vue-apollo-components/src/type-tests/events.vue b/packages/vue-apollo-components/src/type-tests/events.vue new file mode 100644 index 00000000..1791c444 --- /dev/null +++ b/packages/vue-apollo-components/src/type-tests/events.vue @@ -0,0 +1,30 @@ + + + + diff --git a/packages/vue-apollo-components/src/type-tests/negative.vue b/packages/vue-apollo-components/src/type-tests/negative.vue new file mode 100644 index 00000000..323df0db --- /dev/null +++ b/packages/vue-apollo-components/src/type-tests/negative.vue @@ -0,0 +1,67 @@ + + + diff --git a/packages/vue-apollo-components/src/type-tests/single-root.vue b/packages/vue-apollo-components/src/type-tests/single-root.vue new file mode 100644 index 00000000..aeebf0e6 --- /dev/null +++ b/packages/vue-apollo-components/src/type-tests/single-root.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/vue-apollo-components/src/type-tests/untyped.vue b/packages/vue-apollo-components/src/type-tests/untyped.vue new file mode 100644 index 00000000..3cf68a9c --- /dev/null +++ b/packages/vue-apollo-components/src/type-tests/untyped.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/vue-apollo-components/src/type-tests/variables.vue b/packages/vue-apollo-components/src/type-tests/variables.vue new file mode 100644 index 00000000..e919fe41 --- /dev/null +++ b/packages/vue-apollo-components/src/type-tests/variables.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/vue-apollo-components/src/utils.ts b/packages/vue-apollo-components/src/utils.ts new file mode 100644 index 00000000..8b551622 --- /dev/null +++ b/packages/vue-apollo-components/src/utils.ts @@ -0,0 +1,19 @@ +/** + * Layer the dedicated props over the `options` escape hatch. + * + * Only keys the caller passed are applied, so an absent prop cannot overwrite `options`. + */ +export function mergeOptions( + options: TOptions | undefined, + overrides: Record, +): TOptions { + const merged: Record = { ...options } + + for (const [key, value] of Object.entries(overrides)) { + if (value !== undefined) { + merged[key] = value + } + } + + return merged as TOptions +} diff --git a/packages/vue-apollo-components/tsconfig.json b/packages/vue-apollo-components/tsconfig.json new file mode 100644 index 00000000..5267ef35 --- /dev/null +++ b/packages/vue-apollo-components/tsconfig.json @@ -0,0 +1,8 @@ +{ + "references": [ + { "path": "./tsconfig.lib.json" }, + { "path": "./tsconfig.test.json" }, + { "path": "./tsconfig.node.json" } + ], + "files": [] +} diff --git a/packages/vue-apollo-components/tsconfig.lib.json b/packages/vue-apollo-components/tsconfig.lib.json new file mode 100644 index 00000000..def9f886 --- /dev/null +++ b/packages/vue-apollo-components/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../vue-apollo-composable/tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "types": [] + }, + "include": ["src/**/*.ts", "src/**/*.vue"], + "exclude": ["src/**/*.test.ts", "src/test-utils", "src/type-tests"] +} diff --git a/packages/vue-apollo-components/tsconfig.node.json b/packages/vue-apollo-components/tsconfig.node.json new file mode 100644 index 00000000..f394d91c --- /dev/null +++ b/packages/vue-apollo-components/tsconfig.node.json @@ -0,0 +1,7 @@ +{ + "extends": "../vue-apollo-composable/tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["vite.config.ts"] +} diff --git a/packages/vue-apollo-components/tsconfig.test.json b/packages/vue-apollo-components/tsconfig.test.json new file mode 100644 index 00000000..f009300c --- /dev/null +++ b/packages/vue-apollo-components/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "../vue-apollo-composable/tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "types": ["node"] + }, + "include": ["src/test-utils", "src/type-tests", "src/**/*.test.ts", "src/**/*.vue"] +} diff --git a/packages/vue-apollo-components/vite.config.ts b/packages/vue-apollo-components/vite.config.ts new file mode 100644 index 00000000..5ecd87e4 --- /dev/null +++ b/packages/vue-apollo-components/vite.config.ts @@ -0,0 +1,37 @@ +import { resolve } from 'node:path' +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' + +/** Matches the package itself and any subpath, so a deep import is not silently bundled. */ +function externalize(name: string) { + return new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}(?:/|$)`) +} + +export default defineConfig({ + plugins: [ + vue(), + dts({ tsconfigPath: './tsconfig.lib.json' }), + ], + build: { + lib: { + entry: resolve(import.meta.dirname, 'src/index.ts'), + formats: ['es', 'cjs'], + fileName: format => `index.${format === 'es' ? 'mjs' : 'cjs'}`, + }, + sourcemap: true, + rollupOptions: { + // Bundling any of these would ship a second copy alongside the consumer's. + external: [ + externalize('vue'), + externalize('@vue/reactivity'), + externalize('@vue/apollo-composable'), + externalize('@apollo/client'), + externalize('@wry/equality'), + externalize('graphql'), + ], + // The default export is the plugin, kept for v4's `app.use()` setup step. + output: { exports: 'named' }, + }, + }, +}) diff --git a/packages/vue-apollo-components/vitest.config.ts b/packages/vue-apollo-components/vitest.config.ts new file mode 100644 index 00000000..a821e4d8 --- /dev/null +++ b/packages/vue-apollo-components/vitest.config.ts @@ -0,0 +1,9 @@ +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'jsdom', + }, +}) diff --git a/packages/vue-apollo-composable/src/useMutation.test.ts b/packages/vue-apollo-composable/src/useMutation.test.ts new file mode 100644 index 00000000..8beb96e8 --- /dev/null +++ b/packages/vue-apollo-composable/src/useMutation.test.ts @@ -0,0 +1,105 @@ +import type { TypedDocumentNode } from '@apollo/client' +import type { EffectScope } from '@vue/reactivity' +import { ApolloClient, gql, InMemoryCache } from '@apollo/client' +import { MockLink } from '@apollo/client/testing' +import { effectScope } from '@vue/reactivity' +import { afterEach, describe, expect, it } from 'vitest' +import { provideApolloClient } from './useApolloClient.ts' +import { useMutation } from './useMutation.ts' + +const ADD_THING = gql` + mutation AddThing($id: String!) { + addThing(id: $id) { + id + } + } +` as TypedDocumentNode<{ addThing: { id: string } }, { id: string }> + +/** Every call misses the mock, so `mutate()` always fails. */ +function createFailingClient() { + return new ApolloClient({ + link: new MockLink([], { showWarnings: false }), + cache: new InMemoryCache(), + }) +} + +const scopes: EffectScope[] = [] + +function runInScope(fn: () => T): T { + const scope = effectScope() + scopes.push(scope) + return scope.run(fn)! +} + +afterEach(() => { + scopes.splice(0).forEach(scope => scope.stop()) +}) + +describe('useMutation throws: auto', () => { + it('rejects while nothing is listening for errors', async () => { + const client = createFailingClient() + const { mutate } = runInScope(() => + provideApolloClient(client)(() => useMutation(ADD_THING, { variables: { id: 'x' } })), + ) + + await expect(mutate()).rejects.toThrow() + }) + + it('resolves once an error listener is registered', async () => { + const client = createFailingClient() + const { mutate, onError } = runInScope(() => + provideApolloClient(client)(() => useMutation(ADD_THING, { variables: { id: 'x' } })), + ) + onError(() => {}) + + await expect(mutate()).resolves.toMatchObject({ data: undefined }) + }) + + it('rejects again after `off()` removes the listener', async () => { + const client = createFailingClient() + const { mutate, onError } = runInScope(() => + provideApolloClient(client)(() => useMutation(ADD_THING, { variables: { id: 'x' } })), + ) + const { off } = onError(() => {}) + + await expect(mutate()).resolves.toMatchObject({ data: undefined }) + + off() + await expect(mutate()).rejects.toThrow() + }) + + /* + * The event hook drops the handler when the registering scope dies. The listener count + * behind `throws: 'auto'` has to follow, or the mutation keeps resolving for a listener + * that is gone. + */ + it('rejects again after the registering scope is disposed', async () => { + const client = createFailingClient() + const { mutate, onError } = runInScope(() => + provideApolloClient(client)(() => useMutation(ADD_THING, { variables: { id: 'x' } })), + ) + + const listener = effectScope() + listener.run(() => onError(() => {})) + + await expect(mutate()).resolves.toMatchObject({ data: undefined }) + + listener.stop() + await expect(mutate()).rejects.toThrow() + }) + + it('counts each listener, so one leaving does not disarm the rest', async () => { + const client = createFailingClient() + const { mutate, onError } = runInScope(() => + provideApolloClient(client)(() => useMutation(ADD_THING, { variables: { id: 'x' } })), + ) + const first = onError(() => {}) + onError(() => {}) + + first.off() + // Calling the same `off` twice must not decrement past the remaining listener. + first.off() + + await expect(mutate()).resolves.toMatchObject({ data: undefined }) + }) +}) diff --git a/packages/vue-apollo-composable/src/useMutation.ts b/packages/vue-apollo-composable/src/useMutation.ts index 16139e77..d3d30bf3 100644 --- a/packages/vue-apollo-composable/src/useMutation.ts +++ b/packages/vue-apollo-composable/src/useMutation.ts @@ -14,7 +14,7 @@ import type { MaybeRefOrGetter, Ref } from '@vue/reactivity' import type { EventHookOn } from '@vueuse/core' import { computed, getCurrentScope, onScopeDispose, ref, shallowRef, toValue } from '@vue/reactivity' import { nextTick } from '@vue/runtime-core' -import { createEventHook } from '@vueuse/core' +import { createEventHook, tryOnScopeDispose } from '@vueuse/core' import { useApolloClient } from './useApolloClient.ts' import { trackMutation } from './util/loadingTracking.ts' @@ -550,12 +550,20 @@ export function useMutation< function onError(fn: (error: ErrorLike) => void) { errorListenerCount++ const { off } = errorEvent.on(fn) - return { - off: () => { - errorListenerCount-- - off() - }, + let removed = false + + function remove() { + if (removed) { + return + } + removed = true + errorListenerCount-- + off() } + + tryOnScopeDispose(remove) + + return { off: remove } } function hasErrorListeners(): boolean { diff --git a/packages/vue-apollo-composable/src/useQuery.state.test.ts b/packages/vue-apollo-composable/src/useQuery.state.test.ts index 974c30fe..85e20f6d 100644 --- a/packages/vue-apollo-composable/src/useQuery.state.test.ts +++ b/packages/vue-apollo-composable/src/useQuery.state.test.ts @@ -641,6 +641,54 @@ describe('useQuery variable commits', () => { }) // #endregion +// #region Cache hits +describe('useQuery with a result already in the cache', () => { + /* + * The cached result is applied while `useQuery()` is still running, so it reaches the + * event hooks before the caller has had a chance to register any. + */ + it('delivers the cached result to handlers registered after the call', async () => { + const { client } = createMockClient([mock('a', ['a1'])]) + + const warm = createQuery(client, () => ({ variables: { term: 'a' } }))! + await until(() => warm.query.current.value.resultState).toBe('complete') + + const { query } = createQuery(client, () => ({ + variables: { term: 'a' }, + fetchPolicy: 'cache-only', + }))! + + const onResult = vi.fn() + const onCompleteResult = vi.fn() + const onNextState = vi.fn() + query.onResult(onResult) + query.onCompleteResult(onCompleteResult) + query.onNextState(onNextState) + + expect(query.current.value.resultState).toBe('complete') + + await nextTick() + + expect(onResult).toHaveBeenCalledTimes(1) + expect(onCompleteResult).toHaveBeenCalledTimes(1) + expect(onNextState).toHaveBeenCalledTimes(1) + }) + + it('does not replay when the result arrives from the link', async () => { + const { client } = createMockClient([mock('a', ['a1'])]) + const { query } = createQuery(client, () => ({ variables: { term: 'a' } }))! + + const onResult = vi.fn() + query.onResult(onResult) + + await until(() => query.current.value.resultState).toBe('complete') + await promiseTimeout(REQUEST_DELAY) + + expect(onResult).toHaveBeenCalledTimes(1) + }) +}) +// #endregion + // #region Types describe('useQuery state types', () => { it('types the new state fields', () => { diff --git a/packages/vue-apollo-composable/src/useQuery.ts b/packages/vue-apollo-composable/src/useQuery.ts index d1d56d47..35662836 100644 --- a/packages/vue-apollo-composable/src/useQuery.ts +++ b/packages/vue-apollo-composable/src/useQuery.ts @@ -106,9 +106,9 @@ export declare namespace useQuery { /** * Keep previous result while loading new data. * - * The retained result is reported as a normal result — `resultState`, `result` and - * `partial` all describe it — with `isPreviousResult` set to `true` so it can be - * told apart from a fresh one. + * The retained result is reported as a normal result, so `resultState`, `result` and + * `partial` all describe it, with `isPreviousResult` set to `true` so it can be told + * apart from a fresh one. * * @defaultValue false * @group 4. Vue-Apollo @@ -341,6 +341,9 @@ export declare namespace useQuery { * Fires when `resultState` is `'complete'`, `'partial'`, or `'streaming'`. * Does not fire for `'empty'` state or errors. * + * A result served from the cache during the `useQuery()` call is replayed on the + * next tick, so a handler registered right after the call still receives it. + * * @group 6. Events */ onResult: EventHookOn> @@ -1125,6 +1128,9 @@ export function useQueryImpl< const observableQuery = shallowRef>() const subscription = shallowRef() + /** Gates the initial replay below: a delivery after setup makes it redundant. */ + let deliveries = 0 + /** * `isPreviousResult` lives in here rather than its own ref so committing a state is a * single write, and no watcher can catch a fresh result still flagged as retained. @@ -1257,6 +1263,7 @@ export function useQueryImpl< // Via applyState so a query toggled off and back on keeps its previous result. applyState(toResultState(newObservableQuery.getCurrentResult())) subscription.value = newObservableQuery.subscribe((newState) => { + deliveries++ const nextState = toResultState(newState) applyState(nextState) triggerResultEvents(nextState) @@ -1277,6 +1284,25 @@ export function useQueryImpl< } }, { immediate: true, flush: 'sync' }) + /* + * A result already in the cache is applied above, while this function is still running, + * so it reaches the event hooks before the caller can register any. Replay it once they + * can exist. Skipped when the subscription has since delivered a result of its own. + */ + if (currentState.value.resultState !== 'empty') { + const initialState = currentState.value + const deliveriesAtSetup = deliveries + + void nextTick(() => { + if (deliveries !== deliveriesAtSetup) { + return + } + + void nextStateEvent.trigger(current.value) + triggerResultEvents(initialState) + }) + } + /** * React to option changes - reobserve or apply new options as needed. * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3febd71..443f2589 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,23 +34,38 @@ catalogs: specifier: ^2.5.1 version: 2.5.1 '@shikijs/vitepress-twoslash': - specifier: ^3.19.0 - version: 3.19.0 + specifier: ^4.4.1 + version: 4.4.1 + '@types/markdown-it-container': + specifier: ^2.0.10 + version: 2.0.11 '@types/node': specifier: ^24.10.1 version: 24.10.1 + '@vitejs/plugin-vue': + specifier: ^6.0.8 + version: 6.0.8 '@vitest/coverage-v8': specifier: ^4.0.15 version: 4.0.15 + '@vue/reactivity': + specifier: ^3.5.40 + version: 3.5.40 '@vue/runtime-dom': - specifier: ^3.5.25 - version: 3.5.25 + specifier: ^3.5.40 + version: 3.5.40 '@vue/server-renderer': - specifier: ^3.5.25 - version: 3.5.25 + specifier: ^3.5.40 + version: 3.5.40 '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 + '@vueuse/core': + specifier: ^14.4.0 + version: 14.4.0 + '@wry/equality': + specifier: ^0.5.7 + version: 0.5.7 eslint: specifier: ^9.39.1 version: 9.39.1 @@ -72,6 +87,9 @@ catalogs: jsdom: specifier: ^27.2.0 version: 27.2.0 + markdown-it-container: + specifier: ^4.0.0 + version: 4.0.0 nodemon: specifier: ^3.1.11 version: 3.1.11 @@ -90,6 +108,12 @@ catalogs: unbuild: specifier: ^3.6.1 version: 3.6.1 + vite: + specifier: ^8.2.0 + version: 8.2.0 + vite-plugin-dts: + specifier: ^5.0.3 + version: 5.0.3 vitepress: specifier: ^1.6.4 version: 1.6.4 @@ -97,11 +121,17 @@ catalogs: specifier: ^4.0.15 version: 4.0.15 vue: - specifier: ^3.5.25 - version: 3.5.25 + specifier: ^3.5.40 + version: 3.5.40 + vue-component-meta: + specifier: ^3.3.9 + version: 3.3.9 vue-github-button: specifier: ^3.1.3 version: 3.1.3 + vue-tsc: + specifier: ^3.3.9 + version: 3.3.9 importers: @@ -112,7 +142,7 @@ importers: version: 0.5.2 '@antfu/eslint-config': specifier: 'catalog:' - version: 6.5.1(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2)) + version: 6.5.1(@vue/compiler-sfc@3.5.40)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2)) '@microsoft/api-extractor': specifier: 'catalog:' version: 7.55.2(@types/node@24.10.1) @@ -140,6 +170,9 @@ importers: packages/docs: dependencies: + '@vue/apollo-components': + specifier: workspace:* + version: link:../vue-apollo-components '@vue/apollo-composable': specifier: workspace:* version: link:../vue-apollo-composable @@ -155,7 +188,13 @@ importers: version: 6.1.0(@parcel/watcher@2.5.1)(@types/node@24.10.1)(graphql@16.12.0)(typescript@5.9.3) '@shikijs/vitepress-twoslash': specifier: 'catalog:' - version: 3.19.0(typescript@5.9.3) + version: 4.4.1(typescript@5.9.3) + '@types/markdown-it-container': + specifier: 'catalog:' + version: 2.0.11 + '@vueuse/core': + specifier: 'catalog:' + version: 14.4.0(vue@3.5.40(typescript@5.9.3)) graphql: specifier: 'catalog:' version: 16.12.0 @@ -165,6 +204,9 @@ importers: graphql-tag: specifier: 'catalog:' version: 2.12.6(graphql@16.12.0) + markdown-it-container: + specifier: 'catalog:' + version: 4.0.0 typedoc: specifier: 'catalog:' version: 0.28.15(typescript@5.9.3) @@ -176,10 +218,61 @@ importers: version: 1.1.2(typedoc-plugin-markdown@4.9.0(typedoc@0.28.15(typescript@5.9.3))) vitepress: specifier: 'catalog:' - version: 1.6.4(@algolia/client-search@5.46.0)(@types/node@24.10.1)(change-case@5.4.4)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3) + version: 1.6.4(@algolia/client-search@5.46.0)(@types/node@24.10.1)(change-case@5.4.4)(lightningcss@1.33.0)(postcss@8.5.25)(search-insights@2.17.3)(typescript@5.9.3) + vue: + specifier: 'catalog:' + version: 3.5.40(typescript@5.9.3) + vue-component-meta: + specifier: 'catalog:' + version: 3.3.9(typescript@5.9.3) + + packages/vue-apollo-components: + devDependencies: + '@apollo/client': + specifier: 'catalog:' + version: 4.1.3(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.18.3))(graphql@16.12.0)(rxjs@7.8.2) + '@types/node': + specifier: 'catalog:' + version: 24.10.1 + '@vitejs/plugin-vue': + specifier: 'catalog:' + version: 6.0.8(vite@8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.40(typescript@5.9.3)) + '@vue/apollo-composable': + specifier: workspace:* + version: link:../vue-apollo-composable + '@vue/reactivity': + specifier: 'catalog:' + version: 3.5.40 + '@vue/test-utils': + specifier: 'catalog:' + version: 2.4.6 + '@wry/equality': + specifier: 'catalog:' + version: 0.5.7 + graphql: + specifier: 'catalog:' + version: 16.12.0 + jsdom: + specifier: 'catalog:' + version: 27.2.0(postcss@8.5.25) + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite: + specifier: 'catalog:' + version: 8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2) + vite-plugin-dts: + specifier: 'catalog:' + version: 5.0.3(@microsoft/api-extractor@7.55.2(@types/node@24.10.1))(@vue/language-core@3.3.9)(rolldown@1.2.1)(rollup@4.53.3)(typescript@5.9.3)(vite@8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2)) + vitest: + specifier: 'catalog:' + version: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2) vue: specifier: 'catalog:' - version: 3.5.25(typescript@5.9.3) + version: 3.5.40(typescript@5.9.3) + vue-tsc: + specifier: 'catalog:' + version: 3.3.9(typescript@5.9.3) packages/vue-apollo-composable: dependencies: @@ -188,13 +281,13 @@ importers: version: 4.1.3(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.18.3))(graphql@16.12.0)(rxjs@7.8.2) '@vue/reactivity': specifier: ^3.5.0 - version: 3.5.25 + version: 3.5.40 '@vue/runtime-core': specifier: ^3.5.0 - version: 3.5.25 + version: 3.5.40 '@vueuse/core': specifier: ^14.0.0 - version: 14.1.0(vue@3.5.25(typescript@5.9.3)) + version: 14.4.0(vue@3.5.40(typescript@5.9.3)) '@wry/equality': specifier: ^0.5.6 version: 0.5.7 @@ -210,13 +303,13 @@ importers: version: 24.10.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.15(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2)) '@vue/runtime-dom': specifier: 'catalog:' - version: 3.5.25 + version: 3.5.40 '@vue/server-renderer': specifier: 'catalog:' - version: 3.5.25(vue@3.5.25(typescript@5.9.3)) + version: 3.5.40 '@vue/test-utils': specifier: 'catalog:' version: 2.4.6 @@ -234,7 +327,7 @@ importers: version: 5.17.1(graphql@16.12.0) jsdom: specifier: 'catalog:' - version: 27.2.0(postcss@8.5.6) + version: 27.2.0(postcss@8.5.25) nodemon: specifier: 'catalog:' version: 3.1.11 @@ -243,10 +336,10 @@ importers: version: 5.9.3 unbuild: specifier: 'catalog:' - version: 3.6.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) + version: 3.6.1(typescript@5.9.3)(vue-tsc@3.3.9(typescript@5.9.3))(vue@3.5.40(typescript@5.9.3)) vitest: specifier: 'catalog:' - version: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2) + version: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2) packages: @@ -463,12 +556,12 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': @@ -479,8 +572,8 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true @@ -502,8 +595,8 @@ packages: resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@1.0.2': @@ -573,6 +666,15 @@ packages: search-insights: optional: true + '@emnapi/core@2.0.0-alpha.3': + resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + + '@emnapi/runtime@2.0.0-alpha.3': + resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + + '@emnapi/wasi-threads@2.0.1': + resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + '@envelop/core@5.4.0': resolution: {integrity: sha512-/1fat63pySE8rw/dZZArEVytLD90JApY85deDJ0/34gm+yhQ3k70CloSUevxoOE4YCGveG3s9SJJfQeeB4NAtQ==} engines: {node: '>=18.0.0'} @@ -1463,6 +1565,13 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1478,6 +1587,9 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -1571,6 +1683,97 @@ packages: '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + '@rolldown/binding-android-arm64@1.2.1': + resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.1': + resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.1': + resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.1': + resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.1': + resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.2.1': + resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.2.1': + resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.2.1': + resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.2.1': + resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.2.1': + resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.2.1': + resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + + '@rolldown/binding-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -1768,14 +1971,16 @@ packages: '@shikijs/core@2.5.0': resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} - '@shikijs/core@3.19.0': - resolution: {integrity: sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA==} + '@shikijs/core@4.4.1': + resolution: {integrity: sha512-VeR2CY6Nn9/WbisoYLOQZ7HZOnwTrpBuOw4wExjqLnBCi62BNWynBUO6K2uPIASPFJwAv7cX1fUu+LrPlSstcw==} + engines: {node: '>=20'} '@shikijs/engine-javascript@2.5.0': resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} - '@shikijs/engine-javascript@3.19.0': - resolution: {integrity: sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ==} + '@shikijs/engine-javascript@4.4.1': + resolution: {integrity: sha512-6U4lJBh8LTvIkEVqRHv/rr3ruwtO6IweFQt1ME1ntHJMGHS+6N86vfYGO1o8c/DtOCTia2lfhdQBtBrps1sDfQ==} + engines: {node: '>=20'} '@shikijs/engine-oniguruma@2.5.0': resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} @@ -1783,23 +1988,40 @@ packages: '@shikijs/engine-oniguruma@3.19.0': resolution: {integrity: sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg==} + '@shikijs/engine-oniguruma@4.4.1': + resolution: {integrity: sha512-p23RugMKss0r5DAtRJW1yAXUDl60JvhQYV20yuxei//26JyDSJefV3umyWzzwep2weblMnJGDYahuti6XkcMgA==} + engines: {node: '>=20'} + '@shikijs/langs@2.5.0': resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} '@shikijs/langs@3.19.0': resolution: {integrity: sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==} + '@shikijs/langs@4.4.1': + resolution: {integrity: sha512-xb2kCMloBCIraIy2fS5MW0t/BxVY3q2nDyQKBoeSeq6KNrQbShHetCFlw2n35fGIJ6t3+hXDLQogP5ir9O9bvA==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.4.1': + resolution: {integrity: sha512-ko2OfDoG89YuQ7xL5LtcQiWKb7NIv1Ephb7g48TVU198OzAMLC8lXVEwaJGHK4sUMYrfAGJDqYmNLOLiW/Kz8w==} + engines: {node: '>=20'} + '@shikijs/themes@2.5.0': resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} '@shikijs/themes@3.19.0': resolution: {integrity: sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==} + '@shikijs/themes@4.4.1': + resolution: {integrity: sha512-wudOaoFro+/Zl9gQv2W1Ur5XlVduqvTuYLI483Xi0wgc1A+cy1hfB2r6ac6ufBgF+ID7KJEW7L41MHrzQ4wH+w==} + engines: {node: '>=20'} + '@shikijs/transformers@2.5.0': resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} - '@shikijs/twoslash@3.19.0': - resolution: {integrity: sha512-DnkH4slSLPC7dJPhZ9Eofy1X/ZgXiWsvOl/ERK7799ZqXsJwtsq2e8RgHBQUX4Y2lf6aoMojirocLY0AbPF3Dg==} + '@shikijs/twoslash@4.4.1': + resolution: {integrity: sha512-FDv09P7ZYrJpzrJ8LnoT8KZa8ZuWZqAsqWzkVkqExuce9ZsgRZ1966KgniSZM2YJMWudKgNIeadl1TsuBFrVhg==} + engines: {node: '>=20'} peerDependencies: typescript: '>=5.5.0' @@ -1809,8 +2031,13 @@ packages: '@shikijs/types@3.19.0': resolution: {integrity: sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==} - '@shikijs/vitepress-twoslash@3.19.0': - resolution: {integrity: sha512-Su94x/latRAnQxLytbZfV4Cq69uu3POwUrKlUKYeJQX6oc8AEc4ysKoULgdUk8+AsitrTV3EQv3Ok1vgPnFJTg==} + '@shikijs/types@4.4.1': + resolution: {integrity: sha512-GOwCLQDHM5EjGUWNPrhzJbr6JP8V/Dx/CDVkWvbZ1Avw5JFnNUckrgbLmE07qtg4WlW7Q7QFndhjIkeU9XMPvw==} + engines: {node: '>=20'} + + '@shikijs/vitepress-twoslash@4.4.1': + resolution: {integrity: sha512-8mclYzjBm5hRUjbvq6g3cjLCITqAd8+Eac/cefYCwwqdClyBEqJmilNkJ7ZG2ndYS/P14STTU2TED1ZdzpbG0g==} + engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1834,6 +2061,9 @@ packages: peerDependencies: graphql: ^16.0.0 + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -1849,21 +2079,33 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/linkify-it@3.0.5': + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/markdown-it-container@2.0.11': + resolution: {integrity: sha512-tiVQNp2zEHoW7EQa3nNjGRoVk1Uqw9FN7xD39k663lknKZFfByPT3oDyHiaGg+IYu0vSL37PGpDjjMOgtw2GUA==} + + '@types/markdown-it@13.0.9': + resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} + '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@1.0.5': + resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} @@ -1944,13 +2186,14 @@ packages: resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/vfs@1.6.2': - resolution: {integrity: sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g==} + '@typescript/vfs@1.6.4': + resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} peerDependencies: typescript: '*' '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} @@ -1959,6 +2202,13 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + '@vitest/coverage-v8@4.0.15': resolution: {integrity: sha512-FUJ+1RkpTFW7rQITdgTi93qOCWJobWhBirEPCeXh2SW2wsTlFxy51apDz5gzG+ZEYt/THvWeNmhdAoS9DTwpCw==} peerDependencies: @@ -2010,23 +2260,26 @@ packages: '@vitest/utils@4.0.15': resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==} - '@volar/language-core@2.4.26': - resolution: {integrity: sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==} + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} - '@volar/source-map@2.4.26': - resolution: {integrity: sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==} + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.25': - resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + '@vue/compiler-core@3.5.40': + resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} - '@vue/compiler-dom@3.5.25': - resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} + '@vue/compiler-dom@3.5.40': + resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} - '@vue/compiler-sfc@3.5.25': - resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==} + '@vue/compiler-sfc@3.5.40': + resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} - '@vue/compiler-ssr@3.5.25': - resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} + '@vue/compiler-ssr@3.5.40': + resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} '@vue/devtools-api@7.7.9': resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} @@ -2037,30 +2290,23 @@ packages: '@vue/devtools-shared@7.7.9': resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} - '@vue/language-core@3.1.7': - resolution: {integrity: sha512-xbJjFptmuTQD68a3/P70HDb+js61BxYvB3+/h5BflqRNV5dvwH1TZsSsTvMKwFx+QNQf0ndOvD3iih3fHXZYzQ==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@vue/language-core@3.3.9': + resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==} - '@vue/reactivity@3.5.25': - resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==} + '@vue/reactivity@3.5.40': + resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} - '@vue/runtime-core@3.5.25': - resolution: {integrity: sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==} + '@vue/runtime-core@3.5.40': + resolution: {integrity: sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==} - '@vue/runtime-dom@3.5.25': - resolution: {integrity: sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==} + '@vue/runtime-dom@3.5.40': + resolution: {integrity: sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==} - '@vue/server-renderer@3.5.25': - resolution: {integrity: sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==} - peerDependencies: - vue: 3.5.25 + '@vue/server-renderer@3.5.40': + resolution: {integrity: sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==} - '@vue/shared@3.5.25': - resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + '@vue/shared@3.5.40': + resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} '@vue/test-utils@2.4.6': resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} @@ -2068,8 +2314,8 @@ packages: '@vueuse/core@12.8.2': resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} - '@vueuse/core@14.1.0': - resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==} + '@vueuse/core@14.4.0': + resolution: {integrity: sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==} peerDependencies: vue: ^3.5.0 @@ -2117,14 +2363,14 @@ packages: '@vueuse/metadata@12.8.2': resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} - '@vueuse/metadata@14.1.0': - resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==} + '@vueuse/metadata@14.4.0': + resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} - '@vueuse/shared@14.1.0': - resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==} + '@vueuse/shared@14.4.0': + resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} peerDependencies: vue: ^3.5.0 @@ -2215,8 +2461,8 @@ packages: resolution: {integrity: sha512-7ML6fa2K93FIfifG3GMWhDEwT5qQzPTmoHKCTvhzGEwdbQ4n0yYUWZlLYT75WllTGJCJtNUI0C1ybN4BCegqvg==} engines: {node: '>= 14.0.0'} - alien-signals@3.1.1: - resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==} + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} @@ -2441,6 +2687,9 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2611,6 +2860,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2685,6 +2938,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3072,6 +3329,7 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -3443,10 +3701,83 @@ packages: knitwork@1.3.0: resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3454,8 +3785,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkify-it@5.0.2: + resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} listr2@9.0.5: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} @@ -3529,6 +3860,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.1.0: + resolution: {integrity: sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==} + magicast@0.5.1: resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} @@ -3543,8 +3877,11 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + markdown-it-container@4.0.0: + resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==} + + markdown-it@14.3.0: + resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} hasBin: true markdown-table@3.0.4: @@ -3553,8 +3890,8 @@ packages: mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - mdast-util-from-markdown@2.0.2: - resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} mdast-util-frontmatter@2.0.1: resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} @@ -3765,8 +4102,8 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -3852,14 +4189,14 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - oniguruma-parser@0.12.1: - resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} - oniguruma-to-es@4.3.4: - resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} optimism@0.18.1: resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==} @@ -3959,8 +4296,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} pkg-types@1.3.1: @@ -4151,8 +4488,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} preact@10.28.0: @@ -4206,8 +4543,8 @@ packages: regex-utilities@2.3.0: resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - regex@6.0.1: - resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} @@ -4272,6 +4609,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-dts@6.3.0: resolution: {integrity: sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA==} engines: {node: '>=16'} @@ -4345,8 +4687,9 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} - shiki@3.19.0: - resolution: {integrity: sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA==} + shiki@4.4.1: + resolution: {integrity: sha512-rFP+iYKzjLEIqiMiKANhARqiAbk4deDhWnBtnUO/K0D0dPxMGDH4N0FVfBY/VeI+lPrV4wNGCHQZp7EOr7NNBw==} + engines: {node: '>=20'} siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -4517,8 +4860,8 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} tinyrainbow@3.0.3: @@ -4585,18 +4928,18 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - twoslash-protocol@0.3.4: - resolution: {integrity: sha512-HHd7lzZNLUvjPzG/IE6js502gEzLC1x7HaO1up/f72d8G8ScWAs9Yfa97igelQRDl5h9tGcdFsRp+lNVre1EeQ==} + twoslash-protocol@0.3.9: + resolution: {integrity: sha512-9/iwp+CXOnjFMPQuPL5PkuRbZnDoNpBvtJCLs9t8kDYkL3YHujbvnHfZA1i5fApDftVEdBw+T/4F+dH5kIzpYQ==} - twoslash-vue@0.3.4: - resolution: {integrity: sha512-R9hHbmfQMAiHG2UjB0tVFanEzz0SHDa9ZSxowAQFQMPPZSUSuP0meVG2BW2O+q7NAWzya8aJh/eXtPIMX3qsxA==} + twoslash-vue@0.3.9: + resolution: {integrity: sha512-2zO1u4iPhZz9k7ysuDaJL1FUn4SHzm78ZF6/0Q4yFR2VP3NW0JwRpw/4cWqEM6ye3pDf8Zr9lVPBvsX4uK315A==} peerDependencies: - typescript: ^5.5.0 + typescript: ^5.5.0 || ^6.0.0 - twoslash@0.3.4: - resolution: {integrity: sha512-RtJURJlGRxrkJmTcZMjpr7jdYly1rfgpujJr1sBM9ch7SKVht/SjFk23IOAyvwT1NLCk+SJiMrvW4rIAUM2Wug==} + twoslash@0.3.9: + resolution: {integrity: sha512-rDclk+OtzuTX+tnea7DYLCkqGQ3eP0IyfD+kzUJ7t46X/NzlaxwrhecmEBNuSCuEn3V+n1PhcjUUQQ7gUJzX5Q==} peerDependencies: - typescript: ^5.5.0 + typescript: ^5.5.0 || ^6.0.0 type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -4682,6 +5025,40 @@ packages: resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} engines: {node: '>=0.10.0'} + unplugin-dts@1.0.3: + resolution: {integrity: sha512-/GR887wfG4r1cWyt1UZsLRuMIjsmEbGkS9yJrz+0dsToHAYUD5CTyP3JMGVLv25j9K0mJcwAVvZno/aTuSUvNg==} + peerDependencies: + '@microsoft/api-extractor': '>=7' + '@rspack/core': ^1 + '@vue/language-core': ^3.1.5 + esbuild: '*' + rolldown: '*' + rollup: '>=3' + typescript: '>=4' + vite: '>=3' + webpack: ^4 || ^5 + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@rspack/core': + optional: true + '@vue/language-core': + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + untyped@2.0.0: resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} hasBin: true @@ -4713,6 +5090,20 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-dts@5.0.3: + resolution: {integrity: sha512-gIth6NdCEHWPiiRMCK3N6C8WjvdsrtEQrmsiG8h6Ov+lFP+b07Y+wcs9H0H7n146l0PDTYK4cQN1vgeG1pMdRQ==} + peerDependencies: + '@microsoft/api-extractor': '>=7' + rollup: '>=3' + vite: '>=3' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + rollup: + optional: true + vite: + optional: true + vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4784,6 +5175,49 @@ packages: yaml: optional: true + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitepress@1.6.4: resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} hasBin: true @@ -4830,6 +5264,17 @@ packages: jsdom: optional: true + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-meta@3.3.9: + resolution: {integrity: sha512-4u1VmMVjqs9BbKaISBZD7b+dNdqFteh5BYN8xVaXkjjJ66no1sRtDnS6aSs5/P5AzQQGPw10snHTf/z3HpBn2w==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + vue-component-type-helpers@2.2.12: resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} @@ -4847,8 +5292,14 @@ packages: peerDependencies: vue: ^3.0.0 - vue@3.5.25: - resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==} + vue-tsc@3.3.9: + resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.40: + resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -4870,9 +5321,13 @@ packages: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -5094,7 +5549,7 @@ snapshots: dependencies: '@algolia/client-common': 5.46.0 - '@antfu/eslint-config@6.5.1(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2))': + '@antfu/eslint-config@6.5.1(@vue/compiler-sfc@3.5.40)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2))': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 @@ -5103,7 +5558,7 @@ snapshots: '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1)) '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.5.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2)) + '@vitest/eslint-plugin': 1.5.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2)) ansis: 4.2.0 cac: 6.7.14 eslint: 9.39.1(jiti@2.6.1) @@ -5125,7 +5580,7 @@ snapshots: eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))) eslint-plugin-yml: 1.19.0(eslint@9.39.1(jiti@2.6.1)) - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.40)(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 jsonc-eslint-parser: 2.4.2 local-pkg: 1.1.2 @@ -5162,7 +5617,7 @@ snapshots: '@ardatan/relay-compiler@12.0.3(graphql@16.12.0)': dependencies: '@babel/generator': 7.28.5 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.7 '@babel/runtime': 7.28.4 chalk: 4.1.2 fb-watchman: 2.0.2 @@ -5195,7 +5650,7 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -5208,10 +5663,10 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.7 '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@5.5.0) @@ -5223,8 +5678,8 @@ snapshots: '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -5242,7 +5697,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -5250,27 +5705,27 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 - '@babel/parser@7.28.5': + '@babel/parser@7.29.7': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': dependencies: @@ -5282,25 +5737,25 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.7 '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@babel/types@7.28.5': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@bcoe/v8-coverage@1.0.2': {} @@ -5333,9 +5788,9 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.6)': + '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.25)': dependencies: - postcss: 8.5.6 + postcss: 8.5.25 '@csstools/css-tokenizer@3.0.4': {} @@ -5363,6 +5818,22 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@emnapi/core@2.0.0-alpha.3': + dependencies: + '@emnapi/wasi-threads': 2.0.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@2.0.0-alpha.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@2.0.1': + dependencies: + tslib: 2.8.1 + optional: true + '@envelop/core@5.4.0': dependencies: '@envelop/instrumentation': 1.0.0 @@ -5601,7 +6072,7 @@ snapshots: '@eslint/core': 0.17.0 '@eslint/plugin-kit': 0.4.1 github-slugger: 2.0.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-frontmatter: 2.0.1 mdast-util-gfm: 3.1.0 micromark-extension-frontmatter: 2.0.0 @@ -5647,7 +6118,7 @@ snapshots: dependencies: '@babel/generator': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@graphql-codegen/client-preset': 5.2.1(graphql@16.12.0) '@graphql-codegen/core': 5.0.0(graphql@16.12.0) '@graphql-codegen/plugin-helpers': 6.1.0(graphql@16.12.0) @@ -6013,10 +6484,10 @@ snapshots: '@graphql-tools/graphql-tag-pluck@8.3.26(graphql@16.12.0)': dependencies: '@babel/core': 7.28.5 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.7 '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.5) '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@graphql-tools/utils': 10.11.0(graphql@16.12.0) graphql: 16.12.0 tslib: 2.8.1 @@ -6386,6 +6857,13 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + dependencies: + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@tybys/wasm-util': 0.10.3 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6400,6 +6878,8 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@oxc-project/types@0.142.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -6467,6 +6947,57 @@ snapshots: '@repeaterjs/repeater@3.0.6': {} + '@rolldown/binding-android-arm64@1.2.1': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.1': + optional: true + + '@rolldown/binding-darwin-x64@1.2.1': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.1': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.1': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.1': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.1': + optional: true + + '@rolldown/binding-wasm32-wasi@1.2.1': + dependencies: + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.1': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.1': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-alias@5.1.1(rollup@4.53.3)': optionalDependencies: rollup: 4.53.3 @@ -6476,10 +7007,10 @@ snapshots: '@rollup/pluginutils': 5.3.0(rollup@4.53.3) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.5) is-reference: 1.2.1 magic-string: 0.30.21 - picomatch: 4.0.3 + picomatch: 4.0.5 optionalDependencies: rollup: 4.53.3 @@ -6510,7 +7041,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.5 optionalDependencies: rollup: 4.53.3 @@ -6625,14 +7156,15 @@ snapshots: '@shikijs/engine-oniguruma': 2.5.0 '@shikijs/types': 2.5.0 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/core@3.19.0': + '@shikijs/core@4.4.1': dependencies: - '@shikijs/types': 3.19.0 + '@shikijs/primitive': 4.4.1 + '@shikijs/types': 4.4.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 '@shikijs/engine-javascript@2.5.0': @@ -6641,11 +7173,11 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 3.1.1 - '@shikijs/engine-javascript@3.19.0': + '@shikijs/engine-javascript@4.4.1': dependencies: - '@shikijs/types': 3.19.0 + '@shikijs/types': 4.4.1 '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.4 + oniguruma-to-es: 4.3.6 '@shikijs/engine-oniguruma@2.5.0': dependencies: @@ -6657,6 +7189,11 @@ snapshots: '@shikijs/types': 3.19.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@4.4.1': + dependencies: + '@shikijs/types': 4.4.1 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@2.5.0': dependencies: '@shikijs/types': 2.5.0 @@ -6665,6 +7202,16 @@ snapshots: dependencies: '@shikijs/types': 3.19.0 + '@shikijs/langs@4.4.1': + dependencies: + '@shikijs/types': 4.4.1 + + '@shikijs/primitive@4.4.1': + dependencies: + '@shikijs/types': 4.4.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + '@shikijs/themes@2.5.0': dependencies: '@shikijs/types': 2.5.0 @@ -6673,16 +7220,20 @@ snapshots: dependencies: '@shikijs/types': 3.19.0 + '@shikijs/themes@4.4.1': + dependencies: + '@shikijs/types': 4.4.1 + '@shikijs/transformers@2.5.0': dependencies: '@shikijs/core': 2.5.0 '@shikijs/types': 2.5.0 - '@shikijs/twoslash@3.19.0(typescript@5.9.3)': + '@shikijs/twoslash@4.4.1(typescript@5.9.3)': dependencies: - '@shikijs/core': 3.19.0 - '@shikijs/types': 3.19.0 - twoslash: 0.3.4(typescript@5.9.3) + '@shikijs/core': 4.4.1 + '@shikijs/types': 4.4.1 + twoslash: 0.3.9(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6690,28 +7241,33 @@ snapshots: '@shikijs/types@2.5.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@shikijs/types@3.19.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 + + '@shikijs/types@4.4.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 - '@shikijs/vitepress-twoslash@3.19.0(typescript@5.9.3)': + '@shikijs/vitepress-twoslash@4.4.1(typescript@5.9.3)': dependencies: - '@shikijs/twoslash': 3.19.0(typescript@5.9.3) - floating-vue: 5.2.2(vue@3.5.25(typescript@5.9.3)) + '@shikijs/twoslash': 4.4.1(typescript@5.9.3) + floating-vue: 5.2.2(vue@3.5.40(typescript@5.9.3)) lz-string: 1.5.0 - magic-string: 0.30.21 - markdown-it: 14.1.0 - mdast-util-from-markdown: 2.0.2 + magic-string: 1.1.0 + markdown-it: 14.3.0 + mdast-util-from-markdown: 2.0.3 mdast-util-gfm: 3.1.0 mdast-util-to-hast: 13.2.1 ohash: 2.0.11 - shiki: 3.19.0 - twoslash: 0.3.4(typescript@5.9.3) - twoslash-vue: 0.3.4(typescript@5.9.3) - vue: 3.5.25(typescript@5.9.3) + shiki: 4.4.1 + twoslash: 0.3.9(typescript@5.9.3) + twoslash-vue: 0.3.9(typescript@5.9.3) + vue: 3.5.40(typescript@5.9.3) transitivePeerDependencies: - '@nuxt/kit' - supports-color @@ -6731,7 +7287,7 @@ snapshots: eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.3 + picomatch: 4.0.5 '@theguild/federation-composition@0.21.0(graphql@16.12.0)': dependencies: @@ -6743,6 +7299,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/argparse@1.0.38': {} '@types/chai@5.2.3': @@ -6758,14 +7319,25 @@ snapshots: '@types/estree@1.0.8': {} - '@types/hast@3.0.4': + '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 '@types/json-schema@7.0.15': {} + '@types/linkify-it@3.0.5': {} + '@types/linkify-it@5.0.0': {} + '@types/markdown-it-container@2.0.11': + dependencies: + '@types/markdown-it': 13.0.9 + + '@types/markdown-it@13.0.9': + dependencies: + '@types/linkify-it': 3.0.5 + '@types/mdurl': 1.0.5 + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 @@ -6775,6 +7347,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/mdurl@1.0.5': {} + '@types/mdurl@2.0.0': {} '@types/ms@2.1.0': {} @@ -6863,7 +7437,7 @@ snapshots: debug: 4.4.3(supports-color@5.5.0) minimatch: 9.0.5 semver: 7.7.3 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -6885,7 +7459,7 @@ snapshots: '@typescript-eslint/types': 8.48.1 eslint-visitor-keys: 4.2.1 - '@typescript/vfs@1.6.2(typescript@5.9.3)': + '@typescript/vfs@1.6.4(typescript@5.9.3)': dependencies: debug: 4.4.3(supports-color@5.5.0) typescript: 5.9.3 @@ -6894,12 +7468,18 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.10.1))(vue@3.5.25(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.10.1)(lightningcss@1.33.0))(vue@3.5.40(typescript@5.9.3))': dependencies: - vite: 5.4.21(@types/node@24.10.1) - vue: 3.5.25(typescript@5.9.3) + vite: 5.4.21(@types/node@24.10.1)(lightningcss@1.33.0) + vue: 3.5.40(typescript@5.9.3) - '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2))': + '@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.40(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2) + vue: 3.5.40(typescript@5.9.3) + + '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.15 @@ -6912,18 +7492,18 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2) + vitest: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.5.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2))': + '@vitest/eslint-plugin@1.5.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2))': dependencies: '@typescript-eslint/scope-manager': 8.48.1 '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2) + vitest: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -6936,13 +7516,13 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2))': + '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.33.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.15 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.33.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.15': dependencies: @@ -6966,41 +7546,47 @@ snapshots: '@vitest/pretty-format': 4.0.15 tinyrainbow: 3.0.3 - '@volar/language-core@2.4.26': + '@volar/language-core@2.4.28': dependencies: - '@volar/source-map': 2.4.26 + '@volar/source-map': 2.4.28 - '@volar/source-map@2.4.26': {} + '@volar/source-map@2.4.28': {} - '@vue/compiler-core@3.5.25': + '@volar/typescript@2.4.28': dependencies: - '@babel/parser': 7.28.5 - '@vue/shared': 3.5.25 - entities: 4.5.0 + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.40 + entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.25': + '@vue/compiler-dom@3.5.40': dependencies: - '@vue/compiler-core': 3.5.25 - '@vue/shared': 3.5.25 + '@vue/compiler-core': 3.5.40 + '@vue/shared': 3.5.40 - '@vue/compiler-sfc@3.5.25': + '@vue/compiler-sfc@3.5.40': dependencies: - '@babel/parser': 7.28.5 - '@vue/compiler-core': 3.5.25 - '@vue/compiler-dom': 3.5.25 - '@vue/compiler-ssr': 3.5.25 - '@vue/shared': 3.5.25 + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.40 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-ssr': 3.5.40 + '@vue/shared': 3.5.40 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.6 + postcss: 8.5.25 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.25': + '@vue/compiler-ssr@3.5.40': dependencies: - '@vue/compiler-dom': 3.5.25 - '@vue/shared': 3.5.25 + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 '@vue/devtools-api@7.7.9': dependencies: @@ -7020,41 +7606,39 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/language-core@3.1.7(typescript@5.9.3)': + '@vue/language-core@3.3.9': dependencies: - '@volar/language-core': 2.4.26 - '@vue/compiler-dom': 3.5.25 - '@vue/shared': 3.5.25 - alien-signals: 3.1.1 + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 - picomatch: 4.0.3 - optionalDependencies: - typescript: 5.9.3 + picomatch: 4.0.5 - '@vue/reactivity@3.5.25': + '@vue/reactivity@3.5.40': dependencies: - '@vue/shared': 3.5.25 + '@vue/shared': 3.5.40 - '@vue/runtime-core@3.5.25': + '@vue/runtime-core@3.5.40': dependencies: - '@vue/reactivity': 3.5.25 - '@vue/shared': 3.5.25 + '@vue/reactivity': 3.5.40 + '@vue/shared': 3.5.40 - '@vue/runtime-dom@3.5.25': + '@vue/runtime-dom@3.5.40': dependencies: - '@vue/reactivity': 3.5.25 - '@vue/runtime-core': 3.5.25 - '@vue/shared': 3.5.25 + '@vue/reactivity': 3.5.40 + '@vue/runtime-core': 3.5.40 + '@vue/shared': 3.5.40 csstype: 3.2.3 - '@vue/server-renderer@3.5.25(vue@3.5.25(typescript@5.9.3))': + '@vue/server-renderer@3.5.40': dependencies: - '@vue/compiler-ssr': 3.5.25 - '@vue/shared': 3.5.25 - vue: 3.5.25(typescript@5.9.3) + '@vue/compiler-ssr': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/shared': 3.5.40 - '@vue/shared@3.5.25': {} + '@vue/shared@3.5.40': {} '@vue/test-utils@2.4.6': dependencies: @@ -7066,22 +7650,22 @@ snapshots: '@types/web-bluetooth': 0.0.21 '@vueuse/metadata': 12.8.2 '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.25(typescript@5.9.3) + vue: 3.5.40(typescript@5.9.3) transitivePeerDependencies: - typescript - '@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3))': + '@vueuse/core@14.4.0(vue@3.5.40(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.21 - '@vueuse/metadata': 14.1.0 - '@vueuse/shared': 14.1.0(vue@3.5.25(typescript@5.9.3)) - vue: 3.5.25(typescript@5.9.3) + '@vueuse/metadata': 14.4.0 + '@vueuse/shared': 14.4.0(vue@3.5.40(typescript@5.9.3)) + vue: 3.5.40(typescript@5.9.3) '@vueuse/integrations@12.8.2(change-case@5.4.4)(focus-trap@7.6.6)(typescript@5.9.3)': dependencies: '@vueuse/core': 12.8.2(typescript@5.9.3) '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.25(typescript@5.9.3) + vue: 3.5.40(typescript@5.9.3) optionalDependencies: change-case: 5.4.4 focus-trap: 7.6.6 @@ -7090,17 +7674,17 @@ snapshots: '@vueuse/metadata@12.8.2': {} - '@vueuse/metadata@14.1.0': {} + '@vueuse/metadata@14.4.0': {} '@vueuse/shared@12.8.2(typescript@5.9.3)': dependencies: - vue: 3.5.25(typescript@5.9.3) + vue: 3.5.40(typescript@5.9.3) transitivePeerDependencies: - typescript - '@vueuse/shared@14.1.0(vue@3.5.25(typescript@5.9.3))': + '@vueuse/shared@14.4.0(vue@3.5.40(typescript@5.9.3))': dependencies: - vue: 3.5.25(typescript@5.9.3) + vue: 3.5.40(typescript@5.9.3) '@whatwg-node/disposablestack@0.0.6': dependencies: @@ -7207,7 +7791,7 @@ snapshots: '@algolia/requester-fetch': 5.46.0 '@algolia/requester-node-http': 5.46.0 - alien-signals@3.1.1: {} + alien-signals@3.2.1: {} ansi-escapes@7.2.0: dependencies: @@ -7252,14 +7836,14 @@ snapshots: auto-bind@4.0.0: {} - autoprefixer@10.4.22(postcss@8.5.6): + autoprefixer@10.4.22(postcss@8.5.25): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001759 fraction.js: 5.3.4 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 balanced-match@1.0.2: {} @@ -7435,6 +8019,8 @@ snapshots: commondir@1.0.1: {} + compare-versions@6.1.1: {} + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -7498,9 +8084,9 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@7.3.0(postcss@8.5.6): + css-declaration-sorter@7.3.0(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 css-select@5.2.2: dependencies: @@ -7524,58 +8110,58 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.10(postcss@8.5.6): + cssnano-preset-default@7.0.10(postcss@8.5.25): dependencies: browserslist: 4.28.1 - css-declaration-sorter: 7.3.0(postcss@8.5.6) - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 - postcss-calc: 10.1.1(postcss@8.5.6) - postcss-colormin: 7.0.5(postcss@8.5.6) - postcss-convert-values: 7.0.8(postcss@8.5.6) - postcss-discard-comments: 7.0.5(postcss@8.5.6) - postcss-discard-duplicates: 7.0.2(postcss@8.5.6) - postcss-discard-empty: 7.0.1(postcss@8.5.6) - postcss-discard-overridden: 7.0.1(postcss@8.5.6) - postcss-merge-longhand: 7.0.5(postcss@8.5.6) - postcss-merge-rules: 7.0.7(postcss@8.5.6) - postcss-minify-font-values: 7.0.1(postcss@8.5.6) - postcss-minify-gradients: 7.0.1(postcss@8.5.6) - postcss-minify-params: 7.0.5(postcss@8.5.6) - postcss-minify-selectors: 7.0.5(postcss@8.5.6) - postcss-normalize-charset: 7.0.1(postcss@8.5.6) - postcss-normalize-display-values: 7.0.1(postcss@8.5.6) - postcss-normalize-positions: 7.0.1(postcss@8.5.6) - postcss-normalize-repeat-style: 7.0.1(postcss@8.5.6) - postcss-normalize-string: 7.0.1(postcss@8.5.6) - postcss-normalize-timing-functions: 7.0.1(postcss@8.5.6) - postcss-normalize-unicode: 7.0.5(postcss@8.5.6) - postcss-normalize-url: 7.0.1(postcss@8.5.6) - postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) - postcss-ordered-values: 7.0.2(postcss@8.5.6) - postcss-reduce-initial: 7.0.5(postcss@8.5.6) - postcss-reduce-transforms: 7.0.1(postcss@8.5.6) - postcss-svgo: 7.1.0(postcss@8.5.6) - postcss-unique-selectors: 7.0.4(postcss@8.5.6) - - cssnano-utils@5.0.1(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - cssnano@7.1.2(postcss@8.5.6): - dependencies: - cssnano-preset-default: 7.0.10(postcss@8.5.6) + css-declaration-sorter: 7.3.0(postcss@8.5.25) + cssnano-utils: 5.0.1(postcss@8.5.25) + postcss: 8.5.25 + postcss-calc: 10.1.1(postcss@8.5.25) + postcss-colormin: 7.0.5(postcss@8.5.25) + postcss-convert-values: 7.0.8(postcss@8.5.25) + postcss-discard-comments: 7.0.5(postcss@8.5.25) + postcss-discard-duplicates: 7.0.2(postcss@8.5.25) + postcss-discard-empty: 7.0.1(postcss@8.5.25) + postcss-discard-overridden: 7.0.1(postcss@8.5.25) + postcss-merge-longhand: 7.0.5(postcss@8.5.25) + postcss-merge-rules: 7.0.7(postcss@8.5.25) + postcss-minify-font-values: 7.0.1(postcss@8.5.25) + postcss-minify-gradients: 7.0.1(postcss@8.5.25) + postcss-minify-params: 7.0.5(postcss@8.5.25) + postcss-minify-selectors: 7.0.5(postcss@8.5.25) + postcss-normalize-charset: 7.0.1(postcss@8.5.25) + postcss-normalize-display-values: 7.0.1(postcss@8.5.25) + postcss-normalize-positions: 7.0.1(postcss@8.5.25) + postcss-normalize-repeat-style: 7.0.1(postcss@8.5.25) + postcss-normalize-string: 7.0.1(postcss@8.5.25) + postcss-normalize-timing-functions: 7.0.1(postcss@8.5.25) + postcss-normalize-unicode: 7.0.5(postcss@8.5.25) + postcss-normalize-url: 7.0.1(postcss@8.5.25) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.25) + postcss-ordered-values: 7.0.2(postcss@8.5.25) + postcss-reduce-initial: 7.0.5(postcss@8.5.25) + postcss-reduce-transforms: 7.0.1(postcss@8.5.25) + postcss-svgo: 7.1.0(postcss@8.5.25) + postcss-unique-selectors: 7.0.4(postcss@8.5.25) + + cssnano-utils@5.0.1(postcss@8.5.25): + dependencies: + postcss: 8.5.25 + + cssnano@7.1.2(postcss@8.5.25): + dependencies: + cssnano-preset-default: 7.0.10(postcss@8.5.25) lilconfig: 3.1.3 - postcss: 8.5.6 + postcss: 8.5.25 csso@5.0.5: dependencies: css-tree: 2.2.1 - cssstyle@5.3.4(postcss@8.5.6): + cssstyle@5.3.4(postcss@8.5.25): dependencies: '@asamuzakjp/css-color': 4.1.0 - '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6) + '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.25) css-tree: 3.1.0 transitivePeerDependencies: - postcss @@ -7619,6 +8205,8 @@ snapshots: detect-libc@1.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -7686,6 +8274,8 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -7881,7 +8471,7 @@ snapshots: jsonc-eslint-parser: 2.4.2 pathe: 2.0.3 pnpm-workspace-yaml: 1.4.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 yaml: 2.8.2 yaml-eslint-parser: 1.3.2 @@ -7908,7 +8498,7 @@ snapshots: eslint-plugin-unicorn@62.0.0(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@eslint/plugin-kit': 0.4.1 change-case: 5.4.4 @@ -7960,9 +8550,9 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1)): + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.40)(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@vue/compiler-sfc': 3.5.25 + '@vue/compiler-sfc': 3.5.40 eslint: 9.39.1(jiti@2.6.1) eslint-scope@8.4.0: @@ -8091,9 +8681,9 @@ snapshots: transitivePeerDependencies: - encoding - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.5 fetch-blob@3.2.0: dependencies: @@ -8128,11 +8718,11 @@ snapshots: flatted@3.3.3: {} - floating-vue@5.2.2(vue@3.5.25(typescript@5.9.3)): + floating-vue@5.2.2(vue@3.5.40(typescript@5.9.3)): dependencies: '@floating-ui/dom': 1.1.1 - vue: 3.5.25(typescript@5.9.3) - vue-resize: 2.0.0-alpha.1(vue@3.5.25(typescript@5.9.3)) + vue: 3.5.40(typescript@5.9.3) + vue-resize: 2.0.0-alpha.1(vue@3.5.40(typescript@5.9.3)) focus-trap@7.6.6: dependencies: @@ -8281,7 +8871,7 @@ snapshots: hast-util-to-html@9.0.5: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 @@ -8295,7 +8885,7 @@ snapshots: hast-util-whitespace@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 header-case@2.0.4: dependencies: @@ -8493,11 +9083,11 @@ snapshots: jsdoc-type-pratt-parser@6.10.0: {} - jsdom@27.2.0(postcss@8.5.6): + jsdom@27.2.0(postcss@8.5.25): dependencies: '@acemir/cssom': 0.9.28 '@asamuzakjp/dom-selector': 6.7.6 - cssstyle: 5.3.4(postcss@8.5.6) + cssstyle: 5.3.4(postcss@8.5.25) data-urls: 6.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 @@ -8559,16 +9149,67 @@ snapshots: knitwork@1.3.0: {} + kolorist@1.8.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} - linkify-it@5.0.0: + linkify-it@5.0.2: dependencies: uc.micro: 2.1.0 @@ -8648,10 +9289,14 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@1.1.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.1: dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 source-map-js: 1.2.1 make-dir@4.0.0: @@ -8662,11 +9307,13 @@ snapshots: mark.js@8.11.1: {} - markdown-it@14.1.0: + markdown-it-container@4.0.0: {} + + markdown-it@14.3.0: dependencies: argparse: 2.0.1 entities: 4.5.0 - linkify-it: 5.0.0 + linkify-it: 5.0.2 mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 @@ -8680,7 +9327,7 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.2: + mdast-util-from-markdown@2.0.3: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 @@ -8702,7 +9349,7 @@ snapshots: '@types/mdast': 4.0.4 devlop: 1.1.0 escape-string-regexp: 5.0.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 micromark-extension-frontmatter: 2.0.0 transitivePeerDependencies: @@ -8720,7 +9367,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: @@ -8729,7 +9376,7 @@ snapshots: mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -8739,7 +9386,7 @@ snapshots: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -8748,14 +9395,14 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color mdast-util-gfm@3.1.0: dependencies: - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-gfm-autolink-literal: 2.0.1 mdast-util-gfm-footnote: 2.1.0 mdast-util-gfm-strikethrough: 2.0.0 @@ -8772,7 +9419,7 @@ snapshots: mdast-util-to-hast@13.2.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@ungap/structured-clone': 1.3.0 devlop: 1.1.0 @@ -9037,24 +9684,25 @@ snapshots: mitt@3.0.1: {} - mkdist@2.4.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)): + mkdist@2.4.1(typescript@5.9.3)(vue-tsc@3.3.9(typescript@5.9.3))(vue@3.5.40(typescript@5.9.3)): dependencies: - autoprefixer: 10.4.22(postcss@8.5.6) + autoprefixer: 10.4.22(postcss@8.5.25) citty: 0.1.6 - cssnano: 7.1.2(postcss@8.5.6) + cssnano: 7.1.2(postcss@8.5.25) defu: 6.1.4 esbuild: 0.25.12 jiti: 1.21.7 mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 - postcss: 8.5.6 - postcss-nested: 7.0.2(postcss@8.5.6) + postcss: 8.5.25 + postcss-nested: 7.0.2(postcss@8.5.25) semver: 7.7.3 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 optionalDependencies: typescript: 5.9.3 - vue: 3.5.25(typescript@5.9.3) + vue: 3.5.40(typescript@5.9.3) + vue-tsc: 3.3.9(typescript@5.9.3) mlly@1.8.0: dependencies: @@ -9069,7 +9717,7 @@ snapshots: mute-stream@2.0.0: {} - nanoid@3.3.11: {} + nanoid@3.3.16: {} natural-compare@1.4.0: {} @@ -9141,18 +9789,18 @@ snapshots: dependencies: mimic-function: 5.0.1 - oniguruma-parser@0.12.1: {} + oniguruma-parser@0.12.2: {} oniguruma-to-es@3.1.1: dependencies: emoji-regex-xs: 1.0.0 - regex: 6.0.1 + regex: 6.1.0 regex-recursion: 6.0.2 - oniguruma-to-es@4.3.4: + oniguruma-to-es@4.3.6: dependencies: - oniguruma-parser: 0.12.1 - regex: 6.0.1 + oniguruma-parser: 0.12.2 + regex: 6.1.0 regex-recursion: 6.0.2 optimism@0.18.1: @@ -9256,7 +9904,7 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.3: {} + picomatch@4.0.5: {} pkg-types@1.3.1: dependencies: @@ -9276,147 +9924,147 @@ snapshots: dependencies: yaml: 2.8.2 - postcss-calc@10.1.1(postcss@8.5.6): + postcss-calc@10.1.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.5(postcss@8.5.6): + postcss-colormin@7.0.5(postcss@8.5.25): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.8(postcss@8.5.6): + postcss-convert-values@7.0.8(postcss@8.5.25): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.5(postcss@8.5.6): + postcss-discard-comments@7.0.5(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-selector-parser: 7.1.1 - postcss-discard-duplicates@7.0.2(postcss@8.5.6): + postcss-discard-duplicates@7.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 - postcss-discard-empty@7.0.1(postcss@8.5.6): + postcss-discard-empty@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 - postcss-discard-overridden@7.0.1(postcss@8.5.6): + postcss-discard-overridden@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 - postcss-merge-longhand@7.0.5(postcss@8.5.6): + postcss-merge-longhand@7.0.5(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - stylehacks: 7.0.7(postcss@8.5.6) + stylehacks: 7.0.7(postcss@8.5.25) - postcss-merge-rules@7.0.7(postcss@8.5.6): + postcss-merge-rules@7.0.7(postcss@8.5.25): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.25) + postcss: 8.5.25 postcss-selector-parser: 7.1.1 - postcss-minify-font-values@7.0.1(postcss@8.5.6): + postcss-minify-font-values@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.1(postcss@8.5.6): + postcss-minify-gradients@7.0.1(postcss@8.5.25): dependencies: colord: 2.9.3 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.5(postcss@8.5.6): + postcss-minify-params@7.0.5(postcss@8.5.25): dependencies: browserslist: 4.28.1 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.5(postcss@8.5.6): + postcss-minify-selectors@7.0.5(postcss@8.5.25): dependencies: cssesc: 3.0.0 - postcss: 8.5.6 + postcss: 8.5.25 postcss-selector-parser: 7.1.1 - postcss-nested@7.0.2(postcss@8.5.6): + postcss-nested@7.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-selector-parser: 7.1.1 - postcss-normalize-charset@7.0.1(postcss@8.5.6): + postcss-normalize-charset@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 - postcss-normalize-display-values@7.0.1(postcss@8.5.6): + postcss-normalize-display-values@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.1(postcss@8.5.6): + postcss-normalize-positions@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.1(postcss@8.5.6): + postcss-normalize-repeat-style@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.1(postcss@8.5.6): + postcss-normalize-string@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.1(postcss@8.5.6): + postcss-normalize-timing-functions@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.5(postcss@8.5.6): + postcss-normalize-unicode@7.0.5(postcss@8.5.25): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.1(postcss@8.5.6): + postcss-normalize-url@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.1(postcss@8.5.6): + postcss-normalize-whitespace@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.2(postcss@8.5.6): + postcss-ordered-values@7.0.2(postcss@8.5.25): dependencies: - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-reduce-initial@7.0.5(postcss@8.5.6): + postcss-reduce-initial@7.0.5(postcss@8.5.25): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - postcss: 8.5.6 + postcss: 8.5.25 - postcss-reduce-transforms@7.0.1(postcss@8.5.6): + postcss-reduce-transforms@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 postcss-selector-parser@7.1.1: @@ -9424,22 +10072,22 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.1.0(postcss@8.5.6): + postcss-svgo@7.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-value-parser: 4.2.0 svgo: 4.0.0 - postcss-unique-selectors@7.0.4(postcss@8.5.6): + postcss-unique-selectors@7.0.4(postcss@8.5.25): dependencies: - postcss: 8.5.6 + postcss: 8.5.25 postcss-selector-parser: 7.1.1 postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.25: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -9481,7 +10129,7 @@ snapshots: regex-utilities@2.3.0: {} - regex@6.0.1: + regex@6.1.0: dependencies: regex-utilities: 2.3.0 @@ -9537,6 +10185,27 @@ snapshots: rfdc@1.4.1: {} + rolldown@1.2.1: + dependencies: + '@oxc-project/types': 0.142.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.1 + '@rolldown/binding-darwin-arm64': 1.2.1 + '@rolldown/binding-darwin-x64': 1.2.1 + '@rolldown/binding-freebsd-x64': 1.2.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 + '@rolldown/binding-linux-arm64-gnu': 1.2.1 + '@rolldown/binding-linux-arm64-musl': 1.2.1 + '@rolldown/binding-linux-ppc64-gnu': 1.2.1 + '@rolldown/binding-linux-s390x-gnu': 1.2.1 + '@rolldown/binding-linux-x64-gnu': 1.2.1 + '@rolldown/binding-linux-x64-musl': 1.2.1 + '@rolldown/binding-openharmony-arm64': 1.2.1 + '@rolldown/binding-wasm32-wasi': 1.2.1 + '@rolldown/binding-win32-arm64-msvc': 1.2.1 + '@rolldown/binding-win32-x64-msvc': 1.2.1 + rollup-plugin-dts@6.3.0(rollup@4.53.3)(typescript@5.9.3): dependencies: magic-string: 0.30.21 @@ -9632,18 +10301,18 @@ snapshots: '@shikijs/themes': 2.5.0 '@shikijs/types': 2.5.0 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 - shiki@3.19.0: + shiki@4.4.1: dependencies: - '@shikijs/core': 3.19.0 - '@shikijs/engine-javascript': 3.19.0 - '@shikijs/engine-oniguruma': 3.19.0 - '@shikijs/langs': 3.19.0 - '@shikijs/themes': 3.19.0 - '@shikijs/types': 3.19.0 + '@shikijs/core': 4.4.1 + '@shikijs/engine-javascript': 4.4.1 + '@shikijs/engine-oniguruma': 4.4.1 + '@shikijs/langs': 4.4.1 + '@shikijs/themes': 4.4.1 + '@shikijs/types': 4.4.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 siginfo@2.0.0: {} @@ -9740,10 +10409,10 @@ snapshots: strip-json-comments@3.1.1: {} - stylehacks@7.0.7(postcss@8.5.6): + stylehacks@7.0.7(postcss@8.5.25): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.25 postcss-selector-parser: 7.1.1 superjson@2.2.6: @@ -9800,10 +10469,10 @@ snapshots: tinyexec@1.0.2: {} - tinyglobby@0.2.15: + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.0.3: {} @@ -9850,7 +10519,7 @@ snapshots: ts-declaration-location@1.0.7(typescript@5.9.3): dependencies: - picomatch: 4.0.3 + picomatch: 4.0.5 typescript: 5.9.3 ts-log@2.2.7: {} @@ -9859,21 +10528,21 @@ snapshots: tslib@2.8.1: {} - twoslash-protocol@0.3.4: {} + twoslash-protocol@0.3.9: {} - twoslash-vue@0.3.4(typescript@5.9.3): + twoslash-vue@0.3.9(typescript@5.9.3): dependencies: - '@vue/language-core': 3.1.7(typescript@5.9.3) - twoslash: 0.3.4(typescript@5.9.3) - twoslash-protocol: 0.3.4 + '@vue/language-core': 3.3.9 + twoslash: 0.3.9(typescript@5.9.3) + twoslash-protocol: 0.3.9 typescript: 5.9.3 transitivePeerDependencies: - supports-color - twoslash@0.3.4(typescript@5.9.3): + twoslash@0.3.9(typescript@5.9.3): dependencies: - '@typescript/vfs': 1.6.2(typescript@5.9.3) - twoslash-protocol: 0.3.4 + '@typescript/vfs': 1.6.4(typescript@5.9.3) + twoslash-protocol: 0.3.9 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -9894,7 +10563,7 @@ snapshots: dependencies: '@gerrit0/mini-shiki': 3.19.0 lunr: 2.3.9 - markdown-it: 14.1.0 + markdown-it: 14.3.0 minimatch: 9.0.5 typescript: 5.9.3 yaml: 2.8.2 @@ -9909,7 +10578,7 @@ snapshots: ufo@1.6.1: {} - unbuild@3.6.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)): + unbuild@3.6.1(typescript@5.9.3)(vue-tsc@3.3.9(typescript@5.9.3))(vue@3.5.40(typescript@5.9.3)): dependencies: '@rollup/plugin-alias': 5.1.1(rollup@4.53.3) '@rollup/plugin-commonjs': 28.0.9(rollup@4.53.3) @@ -9925,7 +10594,7 @@ snapshots: hookable: 5.5.3 jiti: 2.6.1 magic-string: 0.30.21 - mkdist: 2.4.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-tsc@3.3.9(typescript@5.9.3))(vue@3.5.40(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 @@ -9933,7 +10602,7 @@ snapshots: rollup: 4.53.3 rollup-plugin-dts: 6.3.0(rollup@4.53.3)(typescript@5.9.3) scule: 1.3.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 untyped: 2.0.0 optionalDependencies: typescript: 5.9.3 @@ -9978,6 +10647,33 @@ snapshots: dependencies: normalize-path: 2.1.1 + unplugin-dts@1.0.3(@microsoft/api-extractor@7.55.2(@types/node@24.10.1))(@vue/language-core@3.3.9)(rolldown@1.2.1)(rollup@4.53.3)(typescript@5.9.3)(vite@8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2)): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + '@volar/typescript': 2.4.28 + compare-versions: 6.1.1 + debug: 4.4.3(supports-color@5.5.0) + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.21 + typescript: 5.9.3 + unplugin: 2.3.11 + optionalDependencies: + '@microsoft/api-extractor': 7.55.2(@types/node@24.10.1) + '@vue/language-core': 3.3.9 + rolldown: 1.2.1 + rollup: 4.53.3 + vite: 8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + untyped@2.0.0: dependencies: citty: 0.1.6 @@ -10018,30 +10714,61 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@5.4.21(@types/node@24.10.1): + vite-plugin-dts@5.0.3(@microsoft/api-extractor@7.55.2(@types/node@24.10.1))(@vue/language-core@3.3.9)(rolldown@1.2.1)(rollup@4.53.3)(typescript@5.9.3)(vite@8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2)): + dependencies: + unplugin-dts: 1.0.3(@microsoft/api-extractor@7.55.2(@types/node@24.10.1))(@vue/language-core@3.3.9)(rolldown@1.2.1)(rollup@4.53.3)(typescript@5.9.3)(vite@8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2)) + optionalDependencies: + '@microsoft/api-extractor': 7.55.2(@types/node@24.10.1) + rollup: 4.53.3 + vite: 8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2) + transitivePeerDependencies: + - '@rspack/core' + - '@vue/language-core' + - esbuild + - rolldown + - supports-color + - typescript + - webpack + + vite@5.4.21(@types/node@24.10.1)(lightningcss@1.33.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.6 + postcss: 8.5.25 rollup: 4.53.3 optionalDependencies: '@types/node': 24.10.1 fsevents: 2.3.3 + lightningcss: 1.33.0 - vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2): + vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.33.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.25 rollup: 4.53.3 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.33.0 + yaml: 2.8.2 + + vite@8.2.0(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.1 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.10.1 fsevents: 2.3.3 jiti: 2.6.1 yaml: 2.8.2 - vitepress@1.6.4(@algolia/client-search@5.46.0)(@types/node@24.10.1)(change-case@5.4.4)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3): + vitepress@1.6.4(@algolia/client-search@5.46.0)(@types/node@24.10.1)(change-case@5.4.4)(lightningcss@1.33.0)(postcss@8.5.25)(search-insights@2.17.3)(typescript@5.9.3): dependencies: '@docsearch/css': 3.8.2 '@docsearch/js': 3.8.2(@algolia/client-search@5.46.0)(search-insights@2.17.3) @@ -10050,19 +10777,19 @@ snapshots: '@shikijs/transformers': 2.5.0 '@shikijs/types': 2.5.0 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.10.1))(vue@3.5.25(typescript@5.9.3)) + '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.10.1)(lightningcss@1.33.0))(vue@3.5.40(typescript@5.9.3)) '@vue/devtools-api': 7.7.9 - '@vue/shared': 3.5.25 + '@vue/shared': 3.5.40 '@vueuse/core': 12.8.2(typescript@5.9.3) '@vueuse/integrations': 12.8.2(change-case@5.4.4)(focus-trap@7.6.6)(typescript@5.9.3) focus-trap: 7.6.6 mark.js: 8.11.1 minisearch: 7.2.0 shiki: 2.5.0 - vite: 5.4.21(@types/node@24.10.1) - vue: 3.5.25(typescript@5.9.3) + vite: 5.4.21(@types/node@24.10.1)(lightningcss@1.33.0) + vue: 3.5.40(typescript@5.9.3) optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.25 transitivePeerDependencies: - '@algolia/client-search' - '@types/node' @@ -10090,10 +10817,10 @@ snapshots: - typescript - universal-cookie - vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(yaml@2.8.2): + vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.25))(lightningcss@1.33.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.15 - '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2)) + '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.33.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.15 '@vitest/runner': 4.0.15 '@vitest/snapshot': 4.0.15 @@ -10104,17 +10831,17 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.5 std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 1.0.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 tinyrainbow: 3.0.3 - vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.33.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.1 - jsdom: 27.2.0(postcss@8.5.6) + jsdom: 27.2.0(postcss@8.5.25) transitivePeerDependencies: - jiti - less @@ -10128,6 +10855,16 @@ snapshots: - tsx - yaml + vscode-uri@3.1.0: {} + + vue-component-meta@3.3.9(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.9 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + vue-component-type-helpers@2.2.12: {} vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)): @@ -10146,17 +10883,23 @@ snapshots: dependencies: github-buttons: 2.29.1 - vue-resize@2.0.0-alpha.1(vue@3.5.25(typescript@5.9.3)): + vue-resize@2.0.0-alpha.1(vue@3.5.40(typescript@5.9.3)): + dependencies: + vue: 3.5.40(typescript@5.9.3) + + vue-tsc@3.3.9(typescript@5.9.3): dependencies: - vue: 3.5.25(typescript@5.9.3) + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.9 + typescript: 5.9.3 - vue@3.5.25(typescript@5.9.3): + vue@3.5.40(typescript@5.9.3): dependencies: - '@vue/compiler-dom': 3.5.25 - '@vue/compiler-sfc': 3.5.25 - '@vue/runtime-dom': 3.5.25 - '@vue/server-renderer': 3.5.25(vue@3.5.25(typescript@5.9.3)) - '@vue/shared': 3.5.25 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-sfc': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/server-renderer': 3.5.40 + '@vue/shared': 3.5.40 optionalDependencies: typescript: 5.9.3 @@ -10170,6 +10913,8 @@ snapshots: webidl-conversions@8.0.0: {} + webpack-virtual-modules@0.6.2: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0ecd0caf..391e7283 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,12 +24,17 @@ catalog: '@microsoft/api-extractor-model': ^7.32.2 '@microsoft/tsdoc': ^0.16.0 '@parcel/watcher': ^2.5.1 - '@shikijs/vitepress-twoslash': ^3.19.0 + '@shikijs/vitepress-twoslash': ^4.4.1 + '@types/markdown-it-container': ^2.0.10 '@types/node': ^24.10.1 + '@vitejs/plugin-vue': ^6.0.8 '@vitest/coverage-v8': ^4.0.15 - '@vue/runtime-dom': ^3.5.25 - '@vue/server-renderer': ^3.5.25 + '@vue/reactivity': ^3.5.40 + '@vue/runtime-dom': ^3.5.40 + '@vue/server-renderer': ^3.5.40 '@vue/test-utils': ^2.4.6 + '@vueuse/core': ^14.4.0 + '@wry/equality': ^0.5.7 eslint: ^9.39.1 graphql: ^16.12.0 graphql-sse: ^2.6.0 @@ -37,16 +42,21 @@ catalog: graphql-yoga: ^5.17.1 jiti: ^2.6.1 jsdom: ^27.2.0 + markdown-it-container: ^4.0.0 nodemon: ^3.1.11 typedoc: ^0.28.15 typedoc-plugin-markdown: ^4.9.0 typedoc-vitepress-theme: ^1.1.2 typescript: ^5.9.3 unbuild: ^3.6.1 + vite: ^8.2.0 + vite-plugin-dts: ^5.0.3 vitepress: ^1.6.4 vitest: ^4.0.15 - vue: ^3.5.25 + vue: ^3.5.40 + vue-component-meta: ^3.3.9 vue-github-button: ^3.1.3 + vue-tsc: ^3.3.9 onlyBuiltDependencies: - '@parcel/watcher' - esbuild