nuxtseo-layer-devtools 5.2.0 → 5.2.2
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.
- package/assets/css/global.css +14 -0
- package/components/DevtoolsLayout.vue +66 -21
- package/components/DevtoolsSnippet.vue +1 -1
- package/composables/host.ts +70 -0
- package/composables/rpc.ts +47 -19
- package/composables/shiki.ts +9 -0
- package/package.json +2 -2
package/assets/css/global.css
CHANGED
|
@@ -619,6 +619,20 @@ html.dark {
|
|
|
619
619
|
50% { opacity: 0.4; }
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
/* Remote (standalone) dev-server badge inside the source select */
|
|
623
|
+
.devtools-remote-badge {
|
|
624
|
+
display: inline-flex;
|
|
625
|
+
align-items: center;
|
|
626
|
+
gap: 0.25rem;
|
|
627
|
+
padding: 0.125rem 0.5rem;
|
|
628
|
+
border-radius: 9999px;
|
|
629
|
+
background: var(--color-surface-elevated);
|
|
630
|
+
border: 1px solid var(--color-border);
|
|
631
|
+
color: var(--color-text-muted);
|
|
632
|
+
font-weight: 500;
|
|
633
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
634
|
+
}
|
|
635
|
+
|
|
622
636
|
/* Main content wrapper */
|
|
623
637
|
.devtools-main {
|
|
624
638
|
flex: 1;
|
|
@@ -160,46 +160,61 @@ function disconnectStandalone() {
|
|
|
160
160
|
<span v-if="hasUpdate" class="update-dot" />
|
|
161
161
|
</a>
|
|
162
162
|
</UTooltip>
|
|
163
|
-
<!--
|
|
164
|
-
|
|
163
|
+
<!-- Unified source select. One control for every data source (Local,
|
|
164
|
+
Production, and a remote/standalone dev server). Always a select,
|
|
165
|
+
never a dismissable chip; the standalone disconnect lives in the menu. -->
|
|
166
|
+
<div v-if="isConnected || isStandalone" ref="modeDropdownRef" class="mode-dropdown-wrapper">
|
|
165
167
|
<button type="button" class="devtools-mode-btn" @click="modeDropdownOpen = !modeDropdownOpen">
|
|
166
168
|
<UIcon :name="isProductionMode ? 'carbon:cloud' : 'carbon:laptop'" class="w-3.5 h-3.5" />
|
|
167
169
|
<span class="hidden sm:inline">{{ isProductionMode ? 'Production' : 'Local' }}</span>
|
|
168
|
-
<
|
|
169
|
-
<span class="devtools-production-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
<span v-if="isProductionMode" class="devtools-production-badge">
|
|
171
|
+
<span class="devtools-production-dot" />
|
|
172
|
+
{{ productionHostname }}
|
|
173
|
+
</span>
|
|
174
|
+
<span v-else-if="isStandalone" class="devtools-remote-badge">
|
|
175
|
+
<UIcon name="carbon:plug" class="w-3 h-3" />
|
|
176
|
+
{{ standaloneHostname }}
|
|
177
|
+
</span>
|
|
174
178
|
<UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-50 transition-transform" :class="modeDropdownOpen ? 'rotate-180' : ''" />
|
|
175
179
|
</button>
|
|
176
180
|
<Transition name="dropdown">
|
|
177
181
|
<div v-if="modeDropdownOpen" class="mode-dropdown-menu">
|
|
178
|
-
<button type="button" class="mode-dropdown-item" @click="selectMode('local')">
|
|
182
|
+
<button type="button" class="mode-dropdown-item" :class="!isProductionMode ? 'is-active' : ''" @click="selectMode('local')">
|
|
179
183
|
<UIcon name="carbon:laptop" class="w-4 h-4" />
|
|
180
184
|
<span>Local</span>
|
|
185
|
+
<UIcon v-if="!isProductionMode" name="carbon:checkmark" class="w-3.5 h-3.5 ml-auto text-[var(--seo-green)]" />
|
|
181
186
|
</button>
|
|
182
|
-
<button
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
class="mode-dropdown-item"
|
|
190
|
+
:class="[isProductionMode ? 'is-active' : '', !hasProductionUrl ? 'is-disabled' : '']"
|
|
191
|
+
:disabled="!hasProductionUrl"
|
|
192
|
+
@click="hasProductionUrl && selectMode('production')"
|
|
193
|
+
>
|
|
183
194
|
<UIcon name="carbon:cloud" class="w-4 h-4" />
|
|
184
195
|
<span>Production</span>
|
|
185
|
-
<span class="devtools-production-badge text-[10px]">
|
|
196
|
+
<span v-if="hasProductionUrl" class="devtools-production-badge text-[10px]">
|
|
186
197
|
<span class="devtools-production-dot" />
|
|
187
198
|
{{ productionHostname }}
|
|
188
199
|
</span>
|
|
200
|
+
<span v-else class="text-[10px] opacity-60 ml-auto">Set site url</span>
|
|
201
|
+
<UIcon v-if="isProductionMode" name="carbon:checkmark" class="w-3.5 h-3.5 ml-1 text-[var(--seo-green)]" />
|
|
189
202
|
</button>
|
|
203
|
+
<!-- Remote connection context: shown when reading from a standalone dev server -->
|
|
204
|
+
<template v-if="isStandalone">
|
|
205
|
+
<div class="mode-dropdown-divider" />
|
|
206
|
+
<div class="mode-dropdown-meta">
|
|
207
|
+
<UIcon name="carbon:plug" class="w-3.5 h-3.5 text-[var(--seo-green)]" />
|
|
208
|
+
<span class="truncate">{{ standaloneHostname }}</span>
|
|
209
|
+
</div>
|
|
210
|
+
<button type="button" class="mode-dropdown-item" @click="disconnectStandalone(); modeDropdownOpen = false">
|
|
211
|
+
<UIcon name="carbon:logout" class="w-4 h-4" />
|
|
212
|
+
<span>Disconnect</span>
|
|
213
|
+
</button>
|
|
214
|
+
</template>
|
|
190
215
|
</div>
|
|
191
216
|
</Transition>
|
|
192
217
|
</div>
|
|
193
|
-
<!-- Standalone mode indicator -->
|
|
194
|
-
<div v-if="isStandalone" class="standalone-indicator">
|
|
195
|
-
<UIcon name="carbon:plug" class="w-3.5 h-3.5 text-[var(--seo-green)]" />
|
|
196
|
-
<span class="text-xs font-mono">{{ standaloneHostname }}</span>
|
|
197
|
-
<UTooltip text="Disconnect">
|
|
198
|
-
<button type="button" class="standalone-disconnect" @click="disconnectStandalone">
|
|
199
|
-
<UIcon name="carbon:close" class="w-3 h-3" />
|
|
200
|
-
</button>
|
|
201
|
-
</UTooltip>
|
|
202
|
-
</div>
|
|
203
218
|
</div>
|
|
204
219
|
</div>
|
|
205
220
|
|
|
@@ -408,6 +423,36 @@ function disconnectStandalone() {
|
|
|
408
423
|
color: var(--color-text);
|
|
409
424
|
}
|
|
410
425
|
|
|
426
|
+
.mode-dropdown-item.is-active {
|
|
427
|
+
color: var(--color-text);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.mode-dropdown-item.is-disabled {
|
|
431
|
+
opacity: 0.5;
|
|
432
|
+
cursor: not-allowed;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.mode-dropdown-item.is-disabled:hover {
|
|
436
|
+
background: transparent;
|
|
437
|
+
color: var(--color-text-muted);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.mode-dropdown-divider {
|
|
441
|
+
height: 1px;
|
|
442
|
+
margin: 4px 0;
|
|
443
|
+
background: var(--color-border);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.mode-dropdown-meta {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
gap: 0.5rem;
|
|
450
|
+
padding: 0.25rem 0.625rem;
|
|
451
|
+
font-size: 0.6875rem;
|
|
452
|
+
font-family: var(--font-mono);
|
|
453
|
+
color: var(--color-text-subtle);
|
|
454
|
+
}
|
|
455
|
+
|
|
411
456
|
.dropdown-enter-active {
|
|
412
457
|
transition: opacity 150ms ease, transform 150ms ease;
|
|
413
458
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { NuxtDevtoolsIframeClient } from '@nuxt/devtools-kit/types'
|
|
2
|
+
import type { BirpcReturn } from 'birpc'
|
|
3
|
+
import type { $Fetch } from 'nitropack/types'
|
|
4
|
+
import type { Ref } from 'vue'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A narrow, version-resilient view over the Nuxt DevTools host.
|
|
8
|
+
*
|
|
9
|
+
* The devtools-kit iframe client (`NuxtDevtoolsIframeClient`) only types a small
|
|
10
|
+
* stable surface (`host.app.$fetch`, `host.nuxt.$router`, `host.app.colorMode`,
|
|
11
|
+
* `devtools.extendClientRpc`). Module clients historically reached *past* it into
|
|
12
|
+
* Vue runtime internals (`vueApp._context.provides`, `vueApp._instance.appContext`,
|
|
13
|
+
* `globalProperties.$route`), which differ across @nuxt/devtools versions and
|
|
14
|
+
* threw, wedging the panel. This adapter is the single guarded place that touches
|
|
15
|
+
* those internals; every module reads from it instead.
|
|
16
|
+
*/
|
|
17
|
+
export interface DevtoolsHost {
|
|
18
|
+
/** `host.app.$fetch`, or undefined when the embedded host hasn't exposed it. */
|
|
19
|
+
fetch: $Fetch | undefined
|
|
20
|
+
/** Current host route, derived from the typed `$router`, never `globalProperties.$route`. */
|
|
21
|
+
route: Ref<any> | undefined
|
|
22
|
+
/** Host app base URL, resolved from runtime config then router history, falling back to '/'. */
|
|
23
|
+
baseURL: string
|
|
24
|
+
/** Resolve an app-level provide (e.g. `'usehead'`) regardless of which Vue internal exposes it. */
|
|
25
|
+
inject: <T>(key: string | symbol) => T | undefined
|
|
26
|
+
/** Open a birpc channel to the host, or undefined when the devtools bridge is unavailable. */
|
|
27
|
+
rpc: <ServerFunctions = Record<string, never>, ClientFunctions = Record<string, never>>(
|
|
28
|
+
namespace: string,
|
|
29
|
+
handlers: ClientFunctions,
|
|
30
|
+
) => BirpcReturn<ServerFunctions, ClientFunctions> | undefined
|
|
31
|
+
/** Open a file in the user's editor via the built-in devtools RPC (no-op if unavailable). */
|
|
32
|
+
openInEditor: (path: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a {@link DevtoolsHost} from a raw devtools-kit client.
|
|
37
|
+
*
|
|
38
|
+
* Pure: no import-time side effects, no connection logic. Takes the client as an
|
|
39
|
+
* argument so it can be unit-tested against a malformed host without booting devtools.
|
|
40
|
+
*/
|
|
41
|
+
export function createDevtoolsHost(client: NuxtDevtoolsIframeClient | undefined): DevtoolsHost {
|
|
42
|
+
const nuxt: any = client?.host?.nuxt
|
|
43
|
+
const $router: any = nuxt?.$router
|
|
44
|
+
const baseURL: string = nuxt?.$config?.app?.baseURL
|
|
45
|
+
|| $router?.options?.history?.base
|
|
46
|
+
// @ts-expect-error host.app.baseURL is not in the typed surface; kept only as a last resort
|
|
47
|
+
|| client?.host?.app?.baseURL
|
|
48
|
+
|| '/'
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
// @ts-expect-error host.app.$fetch is untyped on some versions
|
|
52
|
+
fetch: client?.host?.app?.$fetch,
|
|
53
|
+
route: $router?.currentRoute,
|
|
54
|
+
baseURL,
|
|
55
|
+
inject<T>(key: string | symbol): T | undefined {
|
|
56
|
+
const app: any = nuxt?.vueApp
|
|
57
|
+
const provides = app?._context?.provides ?? app?._instance?.appContext?.provides
|
|
58
|
+
return provides?.[key]
|
|
59
|
+
},
|
|
60
|
+
rpc(namespace, handlers) {
|
|
61
|
+
const extend = client?.devtools?.extendClientRpc
|
|
62
|
+
if (typeof extend !== 'function')
|
|
63
|
+
return undefined
|
|
64
|
+
return extend(namespace, handlers) as any
|
|
65
|
+
},
|
|
66
|
+
openInEditor(path) {
|
|
67
|
+
client?.devtools?.rpc?.openInEditor?.(path)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
package/composables/rpc.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import type { NuxtDevtoolsClient } from '@nuxt/devtools-kit/types'
|
|
2
2
|
import type { $Fetch } from 'nitropack/types'
|
|
3
3
|
import type { Ref } from 'vue'
|
|
4
|
+
import type { DevtoolsHost } from './host'
|
|
4
5
|
import { onDevtoolsClientConnected, useDevtoolsClient } from '@nuxt/devtools-kit/iframe-client'
|
|
5
6
|
import { ofetch } from 'ofetch'
|
|
6
7
|
import { ref, watch, watchEffect } from 'vue'
|
|
7
|
-
import {
|
|
8
|
+
import { createDevtoolsHost } from './host'
|
|
9
|
+
import { base, isConnected, path, query, refreshSources, standaloneUrl } from './state'
|
|
8
10
|
|
|
9
11
|
export const appFetch: Ref<$Fetch | undefined> = ref()
|
|
10
12
|
export const devtools: Ref<NuxtDevtoolsClient | undefined> = ref()
|
|
11
13
|
export const colorMode: Ref<'dark' | 'light'> = ref('dark')
|
|
12
14
|
|
|
13
15
|
export interface DevtoolsConnectionOptions {
|
|
14
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Called once the embedded host is connected, with a guarded {@link DevtoolsHost}.
|
|
18
|
+
* Only needed by modules that read host provides or open an RPC channel — route,
|
|
19
|
+
* fetch, colorMode and base are already wired into layer state by the time this fires.
|
|
20
|
+
*/
|
|
21
|
+
onConnected?: (host: DevtoolsHost) => void
|
|
22
|
+
/** Called on every host route change (the layer already refreshes data itself). */
|
|
15
23
|
onRouteChange?: (route: any) => void
|
|
16
24
|
}
|
|
17
25
|
|
|
@@ -33,29 +41,49 @@ export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}):
|
|
|
33
41
|
// Embedded mode: connect via devtools-kit iframe client
|
|
34
42
|
onDevtoolsClientConnected(async (client) => {
|
|
35
43
|
isConnected.value = true
|
|
36
|
-
|
|
37
|
-
appFetch.value =
|
|
44
|
+
const host = createDevtoolsHost(client)
|
|
45
|
+
appFetch.value = host.fetch
|
|
46
|
+
devtools.value = client.devtools
|
|
47
|
+
// Kick an initial data load on connect regardless of route availability, so
|
|
48
|
+
// modules that only needed "refresh once connected" require no onConnected.
|
|
49
|
+
refreshSources()
|
|
50
|
+
|
|
38
51
|
watchEffect(() => {
|
|
39
52
|
colorMode.value = client.host?.app?.colorMode?.value ?? (
|
|
40
53
|
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
41
54
|
)
|
|
42
55
|
})
|
|
43
|
-
devtools.value = client.devtools
|
|
44
|
-
options.onConnected?.(client)
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// The layer owns host route tracking so individual modules never have to
|
|
58
|
+
// reach into the host Vue app. path/query/base feed every module's
|
|
59
|
+
// useAsyncData(watch: [refreshTime]); refreshSources() bumps that clock.
|
|
60
|
+
base.value = host.baseURL
|
|
61
|
+
const applyRoute = (route: any): void => {
|
|
62
|
+
if (!route)
|
|
63
|
+
return
|
|
64
|
+
path.value = route.path || '/'
|
|
65
|
+
query.value = route.query
|
|
66
|
+
refreshSources()
|
|
67
|
+
options.onRouteChange?.(route)
|
|
68
|
+
}
|
|
69
|
+
applyRoute(host.route?.value)
|
|
70
|
+
const $router = client.host?.nuxt?.$router
|
|
71
|
+
if ($router) {
|
|
72
|
+
const removeAfterEach = $router.afterEach((route: any) => applyRoute(route))
|
|
73
|
+
// Clean up when devtools client disconnects
|
|
74
|
+
// @ts-expect-error app:unmount exists at runtime but is not in RuntimeNuxtHooks
|
|
75
|
+
client.host?.nuxt?.hook('app:unmount', removeAfterEach)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Module-specific wiring (host provides / RPC channels) with the guarded host.
|
|
79
|
+
// A throw here must not wedge the panel on "Connecting…": data loading only
|
|
80
|
+
// needs appFetch (set above) plus a refreshTime bump, so recover with a refresh.
|
|
81
|
+
try {
|
|
82
|
+
options.onConnected?.(host)
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error('[nuxt-seo] devtools onConnected handler failed, recovering with data refresh:', err)
|
|
86
|
+
refreshSources()
|
|
59
87
|
}
|
|
60
88
|
})
|
|
61
89
|
|
package/composables/shiki.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { createHighlighterCore } from 'shiki/core'
|
|
|
4
4
|
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
|
|
5
5
|
import { computed, ref, toValue } from 'vue'
|
|
6
6
|
|
|
7
|
+
// Re-exported so consuming layers can type custom grammars without depending on
|
|
8
|
+
// `shiki` directly (it isn't a dependency of the module repos, only of this layer).
|
|
9
|
+
export type { LanguageRegistration } from 'shiki'
|
|
10
|
+
|
|
7
11
|
export const shiki: Ref<HighlighterCore | undefined> = ref()
|
|
8
12
|
|
|
9
13
|
export interface LoadShikiOptions {
|
|
@@ -41,6 +45,11 @@ export function useRenderCodeHighlight(code: MaybeRef<string>, lang: string): Co
|
|
|
41
45
|
return ''
|
|
42
46
|
return shiki.value.codeToHtml(toValue(code) || '', {
|
|
43
47
|
lang,
|
|
48
|
+
// Emit `--shiki-light` / `--shiki-dark` CSS variables instead of a baked-in
|
|
49
|
+
// `color:` so global.css can swap colors per devtools color mode. Without this,
|
|
50
|
+
// shiki writes `color:#xxx` inline and the `.shiki span { color: var(--shiki-light) }`
|
|
51
|
+
// rule resolves to an undefined variable, wiping every syntax color to plain text.
|
|
52
|
+
defaultColor: false,
|
|
44
53
|
themes: {
|
|
45
54
|
light: 'vitesse-light',
|
|
46
55
|
dark: 'vitesse-dark',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxtseo-layer-devtools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.2.
|
|
4
|
+
"version": "5.2.2",
|
|
5
5
|
"description": "Shared Nuxt layer for Nuxt SEO devtools clients.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"shiki": "^4.2.0",
|
|
52
52
|
"tailwindcss": "^4.0.0",
|
|
53
53
|
"ufo": "^1.6.4",
|
|
54
|
-
"nuxtseo-shared": "5.2.
|
|
54
|
+
"nuxtseo-shared": "5.2.2"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"nuxt": "^4.4.7",
|