@gscdump/nuxt 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/app/assets/css/main.css +2 -0
  4. package/app/components/GscAnalyzerPanel.vue +94 -0
  5. package/app/components/GscAnonymizationBanner.vue +46 -0
  6. package/app/components/GscBootProgress.vue +297 -0
  7. package/app/components/GscCommandPalette.vue +77 -0
  8. package/app/components/GscDashboardPage.vue +26 -0
  9. package/app/components/GscEngineBadge.vue +28 -0
  10. package/app/components/GscHero.vue +215 -0
  11. package/app/components/GscLazyTopResult.vue +45 -0
  12. package/app/components/GscPerformanceChart.vue +532 -0
  13. package/app/components/GscPositionDistributionChart.vue +63 -0
  14. package/app/components/GscQueryLabel.vue +63 -0
  15. package/app/components/GscSitePageHeader.vue +40 -0
  16. package/app/components/GscSourceDebugPanel.vue +195 -0
  17. package/app/components/GscSyncStatusCell.vue +54 -0
  18. package/app/components/GscTopRollupCard.vue +90 -0
  19. package/app/composables/_useGscBackfill.ts +111 -0
  20. package/app/composables/_useGscQueryDispatcher.ts +122 -0
  21. package/app/composables/_useGscResource.ts +114 -0
  22. package/app/composables/_useGscSharedSiteResource.ts +136 -0
  23. package/app/composables/useGscAnalytics.ts +197 -0
  24. package/app/composables/useGscAnalyticsClient.ts +9 -0
  25. package/app/composables/useGscAnalyticsConfig.ts +8 -0
  26. package/app/composables/useGscAnalyticsSourceInfo.ts +143 -0
  27. package/app/composables/useGscAnalyzer.ts +374 -0
  28. package/app/composables/useGscAnalyzerBatch.ts +106 -0
  29. package/app/composables/useGscAnalyzerDefs.ts +118 -0
  30. package/app/composables/useGscAuth.ts +115 -0
  31. package/app/composables/useGscConsoleUrl.ts +45 -0
  32. package/app/composables/useGscCountries.ts +47 -0
  33. package/app/composables/useGscCurrentSite.ts +15 -0
  34. package/app/composables/useGscEngine.ts +16 -0
  35. package/app/composables/useGscInspectionHistory.ts +42 -0
  36. package/app/composables/useGscInspections.ts +52 -0
  37. package/app/composables/useGscPanelContext.ts +24 -0
  38. package/app/composables/useGscParquetTable.ts +199 -0
  39. package/app/composables/useGscPeriod.ts +243 -0
  40. package/app/composables/useGscQuery.ts +290 -0
  41. package/app/composables/useGscQueryDispatcher.ts +122 -0
  42. package/app/composables/useGscRequestIndexingInspect.ts +20 -0
  43. package/app/composables/useGscResource.ts +114 -0
  44. package/app/composables/useGscRollup.ts +252 -0
  45. package/app/composables/useGscRollupTable.ts +78 -0
  46. package/app/composables/useGscRowQuery.ts +56 -0
  47. package/app/composables/useGscSearchAppearance.ts +47 -0
  48. package/app/composables/useGscSitemapHistory.ts +41 -0
  49. package/app/composables/useGscSitemaps.ts +45 -0
  50. package/app/composables/useGscTableState.ts +119 -0
  51. package/app/plugins/analytics.ts +57 -0
  52. package/app/utils/anonymization.ts +24 -0
  53. package/app/utils/country-names.ts +56 -0
  54. package/app/utils/gsc-constants.ts +10 -0
  55. package/app/utils/gsc-error.ts +58 -0
  56. package/app/utils/gsc-fetch.ts +94 -0
  57. package/app/utils/gsc-filters.ts +32 -0
  58. package/app/utils/gsc-rows.ts +72 -0
  59. package/app/utils/position.ts +7 -0
  60. package/app/utils/setup-gsc-fetch-auth.ts +62 -0
  61. package/module.ts +81 -0
  62. package/nuxt.config.ts +36 -0
  63. package/package.json +75 -0
  64. package/types.ts +114 -0
@@ -0,0 +1,136 @@
1
+ // Per-site shared resource bag. Consolidates the bind/unbind/refcount pattern
2
+ // that useGscAnalyzer and useGscAnalyticsSourceInfo each reinvented.
3
+ //
4
+ // Two entry points share one underlying bag (a module-scope WeakMap keyed by
5
+ // the NuxtApp instance, so each app gets isolated state and SSR is safe):
6
+ // - useGscSharedSiteResource → composable, refcounted, scope-disposed.
7
+ // - acquireSharedEntry → non-composable acquire for boot-time / non-
8
+ // setup callers (e.g. analyzer createInstance).
9
+ //
10
+ // Refcounting via `onDispose` is opt-in:
11
+ // - omitted → entry persists forever (cheap, share-only; e.g. /source-info)
12
+ // - provided → refcounted; runs onDispose + drops the entry at refs=0
13
+ //
14
+ // Returns a `bound` computed that tracks the entry for the current siteId. The
15
+ // caller wraps it in `computed(() => bound.value?.field ?? default)` to expose
16
+ // reactive fields.
17
+
18
+ import type { ComputedRef } from '@vue/runtime-core'
19
+ import type { NuxtApp } from 'nuxt/app'
20
+
21
+ interface CacheEntry<T> {
22
+ entry: T
23
+ refs: number
24
+ }
25
+
26
+ type NamespaceBag = Map<string, Map<string, CacheEntry<unknown>>>
27
+
28
+ const bags = new WeakMap<NuxtApp, NamespaceBag>()
29
+
30
+ function namespaceMap(namespace: string): Map<string, CacheEntry<unknown>> {
31
+ const app = useNuxtApp()
32
+ let bag = bags.get(app)
33
+ if (!bag) {
34
+ bag = new Map()
35
+ bags.set(app, bag)
36
+ }
37
+ let m = bag.get(namespace)
38
+ if (!m) {
39
+ m = new Map()
40
+ bag.set(namespace, m)
41
+ }
42
+ return m
43
+ }
44
+
45
+ /**
46
+ * Non-composable acquire — returns (and lazily creates) the shared entry for
47
+ * `(namespace, siteId)`. No refcount, no scope dispose. Intended for callers
48
+ * outside a Vue setup scope (analyzer boot IIFE, one-shot loaders) that still
49
+ * need to share state with the reactive `useGscSharedSiteResource` consumers.
50
+ */
51
+ export function acquireSharedEntry<T>(
52
+ namespace: string,
53
+ siteId: string,
54
+ factory: (siteId: string) => T,
55
+ ): T {
56
+ const cache = namespaceMap(namespace) as Map<string, CacheEntry<T>>
57
+ let inst = cache.get(siteId)
58
+ if (!inst) {
59
+ inst = { entry: factory(siteId), refs: 0 }
60
+ cache.set(siteId, inst)
61
+ }
62
+ return inst.entry
63
+ }
64
+
65
+ export interface UseSharedSiteResourceOptions<T> {
66
+ /** Build the entry on first acquire for a site. */
67
+ factory: (siteId: string) => T
68
+ /** When set, refcount the entry and run on the last release (deletes from cache). */
69
+ onDispose?: (entry: T) => Promise<void> | void
70
+ }
71
+
72
+ export interface UseSharedSiteResourceReturn<T> {
73
+ bound: ComputedRef<T | null>
74
+ currentSiteId: Ref<string | null>
75
+ }
76
+
77
+ export function useGscSharedSiteResource<T>(
78
+ namespace: string,
79
+ siteId: MaybeRefOrGetter<string | null | undefined>,
80
+ opts: UseSharedSiteResourceOptions<T>,
81
+ ): UseSharedSiteResourceReturn<T> {
82
+ const cache = namespaceMap(namespace) as Map<string, CacheEntry<T>>
83
+
84
+ const boundRef = shallowRef<T | null>(null)
85
+ const currentSiteId = ref<string | null>(null)
86
+
87
+ function bind(id: string): void {
88
+ unbind()
89
+ let inst = cache.get(id)
90
+ if (!inst) {
91
+ inst = { entry: opts.factory(id), refs: 0 }
92
+ cache.set(id, inst)
93
+ }
94
+ inst.refs++
95
+ boundRef.value = inst.entry
96
+ currentSiteId.value = id
97
+ }
98
+
99
+ function unbind(): void {
100
+ const id = currentSiteId.value
101
+ if (!id) {
102
+ boundRef.value = null
103
+ return
104
+ }
105
+ const inst = cache.get(id)
106
+ boundRef.value = null
107
+ currentSiteId.value = null
108
+ if (!inst)
109
+ return
110
+ inst.refs--
111
+ if (inst.refs <= 0 && opts.onDispose) {
112
+ cache.delete(id)
113
+ void Promise.resolve(opts.onDispose(inst.entry)).catch(() => {})
114
+ }
115
+ }
116
+
117
+ watch(
118
+ () => toValue(siteId),
119
+ (id: string | null | undefined) => {
120
+ if (!id) {
121
+ unbind()
122
+ return
123
+ }
124
+ if (import.meta.client)
125
+ bind(id)
126
+ },
127
+ { immediate: true },
128
+ )
129
+
130
+ onScopeDispose(unbind)
131
+
132
+ return {
133
+ bound: computed(() => boundRef.value) as ComputedRef<T | null>,
134
+ currentSiteId,
135
+ }
136
+ }
@@ -0,0 +1,197 @@
1
+ // Analytics context: sites list + shared boot-progress map + per-site analyzer
2
+ // cache. Provided app-wide by the layer's plugin as `nuxtApp.$gscAnalytics`.
3
+ // Pages reach in via narrower hooks (`useGscSites`, `useGscSite`,
4
+ // `useGscAnalyzer`, `useGscBootProgress`) — no global mutable `currentSite`,
5
+ // no leaked progress writers.
6
+
7
+ import type { SiteListItem } from '@gscdump/contracts'
8
+ import { useGscAnalyticsClient } from './useGscAnalyticsClient'
9
+
10
+ export type { SiteListItem }
11
+
12
+ export type ReadBackend = 'd1' | 'r2'
13
+
14
+ export interface SiteRecord extends SiteListItem {
15
+ /** Canonical GSC site URL. Alias of `label` for back-compat. */
16
+ siteUrl: string
17
+ /**
18
+ * Which backend serves analysis for this site. Carried through from the
19
+ * sites endpoint; when omitted the value defaults to `'r2'` so legacy
20
+ * consumers keep working. Browser-attach eligibility is ultimately
21
+ * decided by the server's source provider via `browserAttachEligible`
22
+ * on `/api/__gsc/sites/:siteId/source-info`, not by this field.
23
+ */
24
+ readBackend: ReadBackend
25
+ syncStatus?: string | null
26
+ }
27
+
28
+ export type SiteLoadStage = 'idle' | 'wasm' | 'manifest' | 'attach' | 'ready' | 'error'
29
+
30
+ export interface SiteLoadProgress {
31
+ siteId: string
32
+ stage: SiteLoadStage
33
+ /** Sub-units (parquet files, rollup JSONs) completed. */
34
+ filesAttached: number
35
+ /** Sub-units expected; 0 while unknown. */
36
+ filesTotal: number
37
+ startedAt: number
38
+ endedAt?: number
39
+ error?: string
40
+ source?: 'duckdb' | 'rollup' | string
41
+ }
42
+
43
+ export interface GscAnalyticsContext {
44
+ sites: Ref<SiteListItem[] | null>
45
+ sitesLoading: Readonly<Ref<boolean>>
46
+ sitesError: Readonly<Ref<Error | null>>
47
+ refreshSites: () => Promise<void>
48
+ progress: Readonly<Ref<Record<string, SiteLoadProgress>>>
49
+ /** Escape hatch for demos/tests — write raw progress entries. */
50
+ patchProgress: (siteId: string, patch: Partial<SiteLoadProgress>) => void
51
+ /** Escape hatch for demos/tests — clear progress map (all or one site). */
52
+ clearProgress: (siteId?: string) => void
53
+ }
54
+
55
+ // Factory exposed so the layer's Nuxt plugin can provide a single app-wide
56
+ // context via `provide: { gscAnalytics: createGscAnalyticsContext() }`.
57
+ export function createGscAnalyticsContext(): GscAnalyticsContext {
58
+ return createContext()
59
+ }
60
+
61
+ /** Resolve the shared context. The layer's plugin guarantees this is provided. */
62
+ export function useGscAnalyticsContext(): GscAnalyticsContext {
63
+ return useNuxtApp().$gscAnalytics
64
+ }
65
+
66
+ /** Read-only sites list + refresh. */
67
+ export function useGscSites(): {
68
+ sites: Ref<SiteListItem[] | null>
69
+ loading: Readonly<Ref<boolean>>
70
+ error: Readonly<Ref<Error | null>>
71
+ refresh: () => Promise<void>
72
+ } {
73
+ const { sites, sitesLoading, sitesError, refreshSites } = useGscAnalyticsContext()
74
+ return { sites, loading: sitesLoading, error: sitesError, refresh: refreshSites }
75
+ }
76
+
77
+ /** Derived site record for a given id. Non-mutating; returns `null` until the list resolves. */
78
+ export function useGscSite(siteId: MaybeRefOrGetter<string | null | undefined>): Ref<SiteRecord | null> {
79
+ const { sites } = useGscAnalyticsContext()
80
+ return computed(() => {
81
+ const id = toValue(siteId)
82
+ if (!id)
83
+ return null
84
+ const found = sites.value?.find((s: SiteListItem) => s.id === id)
85
+ if (found)
86
+ return toSiteRecord(found)
87
+ // List not yet loaded — stamp a minimal record so downstream queries can
88
+ // dispatch against the id. Gets upgraded when sites resolves.
89
+ return {
90
+ id,
91
+ label: id,
92
+ hostname: id,
93
+ propertyType: 'url-prefix',
94
+ siteUrl: id,
95
+ readBackend: 'r2',
96
+ }
97
+ })
98
+ }
99
+
100
+ function toSiteRecord(s: SiteListItem): SiteRecord {
101
+ return {
102
+ ...s,
103
+ siteUrl: s.label,
104
+ readBackend: s.readBackend ?? 'r2',
105
+ }
106
+ }
107
+
108
+ /** Read-only boot-progress map for UI indicators. */
109
+ export function useGscBootProgress(): {
110
+ progress: Readonly<Ref<Record<string, SiteLoadProgress>>>
111
+ entries: ComputedRef<SiteLoadProgress[]>
112
+ allReady: ComputedRef<boolean>
113
+ anyActive: ComputedRef<boolean>
114
+ } {
115
+ const { progress } = useGscAnalyticsContext()
116
+ const entries = computed<SiteLoadProgress[]>(() => {
117
+ const all = Object.values(progress.value) as SiteLoadProgress[]
118
+ return all.sort((a, b) => a.startedAt - b.startedAt)
119
+ })
120
+ const allReady = computed(() =>
121
+ entries.value.length > 0 && entries.value.every((s: SiteLoadProgress) => s.stage === 'ready'),
122
+ )
123
+ const anyActive = computed(() =>
124
+ entries.value.some((s: SiteLoadProgress) => s.stage !== 'ready' && s.stage !== 'idle' && s.stage !== 'error'),
125
+ )
126
+ return { progress, entries, allReady, anyActive }
127
+ }
128
+
129
+ function createContext(): GscAnalyticsContext {
130
+ const sites = ref<SiteListItem[] | null>(null)
131
+ const sitesLoading = ref(false)
132
+ const sitesError = ref<Error | null>(null)
133
+ const progress = ref<Record<string, SiteLoadProgress>>({})
134
+
135
+ async function refreshSites(): Promise<void> {
136
+ sitesLoading.value = true
137
+ sitesError.value = null
138
+ sites.value = await useGscAnalyticsClient().listSites().catch((err) => {
139
+ sitesError.value = err instanceof Error ? err : new Error(String(err))
140
+ return null
141
+ })
142
+ sitesLoading.value = false
143
+ }
144
+
145
+ function patchProgress(siteId: string, patch: Partial<SiteLoadProgress>): void {
146
+ const prev = progress.value[siteId]
147
+ progress.value = {
148
+ ...progress.value,
149
+ [siteId]: {
150
+ siteId,
151
+ stage: 'idle',
152
+ filesAttached: 0,
153
+ filesTotal: 0,
154
+ startedAt: prev?.startedAt ?? Date.now(),
155
+ ...prev,
156
+ ...patch,
157
+ },
158
+ }
159
+ }
160
+
161
+ function clearProgress(siteId?: string): void {
162
+ if (!siteId) {
163
+ progress.value = {}
164
+ return
165
+ }
166
+ const next = { ...progress.value }
167
+ delete next[siteId]
168
+ progress.value = next
169
+ }
170
+
171
+ // Lazy-load: defer refreshSites until something actually reads `sites`.
172
+ // Eager kick-off used to race host plugins that set auth via `setGscAuth`;
173
+ // first call would 401 and the rest of the session would silently degrade.
174
+ // Triggering on first read defers the request to a microtask after plugin
175
+ // setup, so auth state is in place.
176
+ let kickedOff = false
177
+ function maybeKickOff(): void {
178
+ if (kickedOff || !import.meta.client)
179
+ return
180
+ kickedOff = true
181
+ refreshSites()
182
+ }
183
+ const lazySites = computed<SiteListItem[] | null>(() => {
184
+ maybeKickOff()
185
+ return sites.value
186
+ })
187
+
188
+ return {
189
+ sites: lazySites as Readonly<Ref<SiteListItem[] | null>> as unknown as Ref<SiteListItem[] | null>,
190
+ sitesLoading: sitesLoading as Readonly<Ref<boolean>>,
191
+ sitesError: sitesError as Readonly<Ref<Error | null>>,
192
+ refreshSites,
193
+ progress: progress as Readonly<Ref<Record<string, SiteLoadProgress>>>,
194
+ patchProgress,
195
+ clearProgress,
196
+ }
197
+ }
@@ -0,0 +1,9 @@
1
+ // Reader for the layer's `AnalyticsClient`. Built once per NuxtApp by the
2
+ // layer plugin and provided as `$gscAnalyticsClient`. Hosts override by
3
+ // providing their own from a later plugin (e.g. to swap the SDK transport).
4
+
5
+ import type { AnalyticsClient } from '@gscdump/sdk'
6
+
7
+ export function useGscAnalyticsClient(): AnalyticsClient {
8
+ return useNuxtApp().$gscAnalyticsClient as AnalyticsClient
9
+ }
@@ -0,0 +1,8 @@
1
+ // Typed reader for `runtimeConfig.public.analytics`. Single accessor so leaf
2
+ // composables stop re-typing the cast and silently drift when keys are added.
3
+
4
+ import type { GscAnalyticsRuntimeConfig } from '../../types'
5
+
6
+ export function useGscAnalyticsConfig(): GscAnalyticsRuntimeConfig {
7
+ return useRuntimeConfig().public.analytics as unknown as GscAnalyticsRuntimeConfig
8
+ }
@@ -0,0 +1,143 @@
1
+ // Per-site reactive probe of what the server-resolved AnalysisQuerySource
2
+ // can do. The layer exposes source shape (kind + analyzer set); tier logic
3
+ // lives in the host.
4
+ //
5
+ // Use this to render locked/ghost UI upfront without issuing a doomed
6
+ // analyze() call for each panel.
7
+ //
8
+ // The reactive composable and the non-reactive `loadSourceInfoFor` (used by
9
+ // `useGscAnalyzer.createInstance`) share one entry per site via the shared
10
+ // site-resource seam, so they collapse to one network read per session.
11
+
12
+ import type { SourceCapabilities } from '@gscdump/analysis'
13
+ import { acquireSharedEntry, useGscSharedSiteResource } from './_useGscSharedSiteResource'
14
+ import { useGscAnalyticsClient } from './useGscAnalyticsClient'
15
+
16
+ export interface GscAnalyticsSourceInfo {
17
+ /** Source implementation name — e.g. "gsc-api", "engine", "browser". */
18
+ name: string
19
+ /** `row` = GSC API / in-memory; `sql` = DuckDB/SQLite-backed. */
20
+ kind: 'row' | 'sql'
21
+ capabilities: SourceCapabilities
22
+ /** Analyzer ids runnable against this source, from the default registry. */
23
+ supportedAnalyzerIds: string[]
24
+ /** Host declared attach hint — client should boot DuckDB-WASM and attach parquets. */
25
+ browserAttachEligible: boolean
26
+ /**
27
+ * Host-controlled identity attrs echoed back for debug/display (e.g.
28
+ * `{ tier: 'free' | 'pro' }`). Layer treats as opaque — never use these
29
+ * for gating on the client, the source provider is the gate.
30
+ */
31
+ identityAttrs?: Record<string, unknown> | null
32
+ }
33
+
34
+ interface GscAnalyticsSourceInfoState {
35
+ info: Ref<GscAnalyticsSourceInfo | null>
36
+ loading: Ref<boolean>
37
+ error: Ref<Error | null>
38
+ refresh: () => Promise<void>
39
+ /** Convenience predicate for panel gating: true when the analyzer id is in `supportedAnalyzerIds`. */
40
+ supports: (analyzerId: MaybeRefOrGetter<string>) => ComputedRef<boolean>
41
+ }
42
+
43
+ interface SourceInfoEntry {
44
+ info: Ref<GscAnalyticsSourceInfo | null>
45
+ loading: Ref<boolean>
46
+ error: Ref<Error | null>
47
+ pending: Ref<Promise<void> | null>
48
+ }
49
+
50
+ const SOURCE_INFO_NAMESPACE = 'source-info'
51
+
52
+ function createEntry(): SourceInfoEntry {
53
+ return {
54
+ info: ref<GscAnalyticsSourceInfo | null>(null),
55
+ loading: ref(false),
56
+ error: ref<Error | null>(null),
57
+ pending: shallowRef<Promise<void> | null>(null),
58
+ }
59
+ }
60
+
61
+ function fetchInto(entry: SourceInfoEntry, siteId: string): Promise<void> {
62
+ if (entry.pending.value)
63
+ return entry.pending.value
64
+ entry.loading.value = true
65
+ entry.error.value = null
66
+ const p = (useGscAnalyticsClient().getSourceInfo(siteId) as Promise<GscAnalyticsSourceInfo>)
67
+ .then((data) => {
68
+ entry.info.value = data
69
+ })
70
+ .catch((err: unknown) => {
71
+ entry.error.value = err instanceof Error ? err : new Error(String(err))
72
+ })
73
+ .finally(() => {
74
+ entry.loading.value = false
75
+ entry.pending.value = null
76
+ })
77
+ entry.pending.value = p
78
+ return p
79
+ }
80
+
81
+ /**
82
+ * Non-reactive source-info loader for callers outside a Vue scope (e.g. the
83
+ * analyzer's boot IIFE in `useGscAnalyzer.createInstance`). Shares the same
84
+ * per-site cache `useGscAnalyticsSourceInfo` consumes via the shared
85
+ * site-resource seam, so gating UI and the analyzer mode probe collapse to
86
+ * one network read per site per session.
87
+ */
88
+ export async function loadSourceInfoFor(siteId: string): Promise<GscAnalyticsSourceInfo> {
89
+ const entry = acquireSharedEntry(SOURCE_INFO_NAMESPACE, siteId, createEntry)
90
+ if (entry.info.value)
91
+ return entry.info.value
92
+ await fetchInto(entry, siteId)
93
+ if (entry.error.value)
94
+ throw entry.error.value
95
+ if (!entry.info.value)
96
+ throw new Error(`loadSourceInfoFor(${siteId}): no info after fetch`)
97
+ return entry.info.value
98
+ }
99
+
100
+ export function useGscAnalyticsSourceInfo(
101
+ siteId: MaybeRefOrGetter<string | null | undefined>,
102
+ ): GscAnalyticsSourceInfoState {
103
+ const { bound } = useGscSharedSiteResource<SourceInfoEntry>(SOURCE_INFO_NAMESPACE, siteId, {
104
+ factory: () => createEntry(),
105
+ // No onDispose: source-info is cheap and persistent across the session.
106
+ })
107
+
108
+ if (import.meta.client) {
109
+ watch(bound, (entry: SourceInfoEntry | null) => {
110
+ if (!entry)
111
+ return
112
+ const id = toValue(siteId)
113
+ if (!id)
114
+ return
115
+ if (entry.info.value == null && entry.pending.value == null && entry.error.value == null)
116
+ void fetchInto(entry, id)
117
+ }, { immediate: true })
118
+ }
119
+
120
+ const info = computed(() => bound.value?.info.value ?? null) as unknown as Ref<GscAnalyticsSourceInfo | null>
121
+ const loading = computed(() => bound.value?.loading.value ?? false) as unknown as Ref<boolean>
122
+ const error = computed(() => bound.value?.error.value ?? null) as unknown as Ref<Error | null>
123
+
124
+ async function refresh(): Promise<void> {
125
+ const entry = bound.value
126
+ const id = toValue(siteId)
127
+ if (!entry || !id)
128
+ return
129
+ entry.info.value = null
130
+ await fetchInto(entry, id)
131
+ }
132
+
133
+ function supports(analyzerId: MaybeRefOrGetter<string>): ComputedRef<boolean> {
134
+ return computed(() => {
135
+ const list = bound.value?.info.value?.supportedAnalyzerIds
136
+ if (!list)
137
+ return false
138
+ return list.includes(toValue(analyzerId))
139
+ })
140
+ }
141
+
142
+ return { info, loading, error, refresh, supports }
143
+ }