nuxtseo-layer-devtools 0.4.4 → 0.5.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.
- package/components/DevtoolsLayout.vue +48 -7
- package/components/DevtoolsModuleSplash.vue +135 -9
- package/components/DevtoolsPanel.vue +13 -3
- package/components/DevtoolsPlaygrounds.vue +99 -0
- package/components/DevtoolsTroubleshooting.vue +332 -0
- package/composables/modules.ts +48 -13
- package/composables/package-manager.ts +41 -0
- package/composables/update-check.ts +39 -0
- package/error.vue +4 -1
- package/package.json +3 -2
- package/skills/devtools-layer-skilld/SKILL.md +17 -8
- package/skills/devtools-layer-skilld/reference.md +70 -7
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onClickOutside } from '@vueuse/core'
|
|
3
3
|
import { computed, ref } from 'vue'
|
|
4
|
-
import { fetchInstalledModules, showModuleSplash } from '../composables/modules'
|
|
4
|
+
import { fetchInstalledModules, findModuleByName, showModuleSplash } from '../composables/modules'
|
|
5
5
|
import { colorMode } from '../composables/rpc'
|
|
6
6
|
import { hasProductionUrl, isConnected, isProductionMode, isStandalone, path, previewSource, productionUrl, standaloneUrl } from '../composables/state'
|
|
7
|
+
import { useModuleUpdate } from '../composables/update-check'
|
|
7
8
|
|
|
8
9
|
export interface DevtoolsNavItem {
|
|
9
10
|
value: string
|
|
@@ -35,6 +36,10 @@ const emit = defineEmits<{
|
|
|
35
36
|
refresh: []
|
|
36
37
|
}>()
|
|
37
38
|
|
|
39
|
+
const moduleInfo = computed(() => moduleName ? findModuleByName(moduleName) : undefined)
|
|
40
|
+
const npmPackage = computed(() => moduleInfo.value?.npm)
|
|
41
|
+
const { hasUpdate, latestVersion } = useModuleUpdate(npmPackage.value, version)
|
|
42
|
+
|
|
38
43
|
// Fetch installed modules for the splash screen
|
|
39
44
|
fetchInstalledModules()
|
|
40
45
|
|
|
@@ -127,12 +132,21 @@ function disconnectStandalone() {
|
|
|
127
132
|
</span>
|
|
128
133
|
<UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-50 transition-transform" :class="showModuleSplash ? 'rotate-180' : ''" />
|
|
129
134
|
</button>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
<UTooltip v-if="version" :text="hasUpdate ? `Update available: v${latestVersion}` : `v${version}`">
|
|
136
|
+
<a
|
|
137
|
+
:href="hasUpdate && npmPackage ? `https://npmjs.com/package/${npmPackage}` : undefined"
|
|
138
|
+
:target="hasUpdate ? '_blank' : undefined"
|
|
139
|
+
rel="noopener"
|
|
140
|
+
class="version-badge-wrapper"
|
|
141
|
+
>
|
|
142
|
+
<UBadge
|
|
143
|
+
class="font-mono text-[10px] sm:text-xs hidden sm:inline-flex"
|
|
144
|
+
>
|
|
145
|
+
v{{ version }}
|
|
146
|
+
</UBadge>
|
|
147
|
+
<span v-if="hasUpdate" class="update-dot" />
|
|
148
|
+
</a>
|
|
149
|
+
</UTooltip>
|
|
136
150
|
<!-- Mode dropdown: embedded with production URL -->
|
|
137
151
|
<div v-if="hasProductionUrl && !isStandalone" ref="modeDropdownRef" class="mode-dropdown-wrapper">
|
|
138
152
|
<button type="button" class="devtools-mode-btn" @click="modeDropdownOpen = !modeDropdownOpen">
|
|
@@ -279,6 +293,9 @@ function disconnectStandalone() {
|
|
|
279
293
|
<DevtoolsLoading v-show="!showStandaloneSetup && loading" />
|
|
280
294
|
<div v-show="!showStandaloneSetup && !loading">
|
|
281
295
|
<slot />
|
|
296
|
+
<div v-if="activeTab === 'debug' && moduleName" class="devtools-troubleshooting-section">
|
|
297
|
+
<DevtoolsTroubleshooting :module-name="moduleName" :version="version" />
|
|
298
|
+
</div>
|
|
282
299
|
</div>
|
|
283
300
|
</main>
|
|
284
301
|
</div>
|
|
@@ -289,6 +306,25 @@ function disconnectStandalone() {
|
|
|
289
306
|
</template>
|
|
290
307
|
|
|
291
308
|
<style scoped>
|
|
309
|
+
.version-badge-wrapper {
|
|
310
|
+
position: relative;
|
|
311
|
+
display: inline-flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
text-decoration: none;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.update-dot {
|
|
317
|
+
position: absolute;
|
|
318
|
+
top: -2px;
|
|
319
|
+
right: -2px;
|
|
320
|
+
width: 7px;
|
|
321
|
+
height: 7px;
|
|
322
|
+
border-radius: 50%;
|
|
323
|
+
background: var(--seo-green);
|
|
324
|
+
border: 1.5px solid var(--color-surface);
|
|
325
|
+
pointer-events: none;
|
|
326
|
+
}
|
|
327
|
+
|
|
292
328
|
.devtools-module-switcher {
|
|
293
329
|
display: flex;
|
|
294
330
|
align-items: center;
|
|
@@ -418,4 +454,9 @@ function disconnectStandalone() {
|
|
|
418
454
|
.standalone-path-input:focus {
|
|
419
455
|
border-color: var(--seo-green);
|
|
420
456
|
}
|
|
457
|
+
|
|
458
|
+
.devtools-troubleshooting-section {
|
|
459
|
+
padding: 1.5rem 1rem 1rem;
|
|
460
|
+
max-width: 48rem;
|
|
461
|
+
}
|
|
421
462
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { onClickOutside } from '@vueuse/core'
|
|
2
|
+
import { onClickOutside, useClipboard } from '@vueuse/core'
|
|
3
3
|
import { computed, ref } from 'vue'
|
|
4
4
|
import { moduleCatalog, showModuleSplash, switchToModule } from '../composables/modules'
|
|
5
5
|
import { isConnected } from '../composables/state'
|
|
@@ -16,8 +16,28 @@ onClickOutside(panelRef, () => {
|
|
|
16
16
|
const coreModules = computed(() => moduleCatalog.value.filter(m => !m.pro))
|
|
17
17
|
const proModules = computed(() => moduleCatalog.value.filter(m => m.pro))
|
|
18
18
|
|
|
19
|
+
const selectedForInstall = ref(new Set<string>())
|
|
20
|
+
|
|
21
|
+
const installCommand = computed(() => {
|
|
22
|
+
if (!selectedForInstall.value.size)
|
|
23
|
+
return ''
|
|
24
|
+
const names = Array.from(selectedForInstall.value, name => moduleCatalog.value.find(m => m.name === name)?.npm).filter(Boolean)
|
|
25
|
+
return `npx nuxt module add ${names.join(' ')}`
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const { copy, copied } = useClipboard({ source: installCommand })
|
|
29
|
+
|
|
19
30
|
function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
20
|
-
if (!mod.installed
|
|
31
|
+
if (!mod.installed) {
|
|
32
|
+
const set = new Set(selectedForInstall.value)
|
|
33
|
+
if (set.has(mod.name))
|
|
34
|
+
set.delete(mod.name)
|
|
35
|
+
else
|
|
36
|
+
set.add(mod.name)
|
|
37
|
+
selectedForInstall.value = set
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
if (mod.name === props.currentModule)
|
|
21
41
|
return
|
|
22
42
|
switchToModule(mod.name)
|
|
23
43
|
}
|
|
@@ -50,10 +70,11 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
50
70
|
class="splash-module"
|
|
51
71
|
:class="{
|
|
52
72
|
'is-current': mod.name === currentModule,
|
|
53
|
-
'is-unavailable': !mod.installed,
|
|
73
|
+
'is-unavailable': !mod.installed && !selectedForInstall.has(mod.name) && mod.name !== currentModule,
|
|
74
|
+
'is-selected-install': selectedForInstall.has(mod.name),
|
|
54
75
|
'is-switchable': mod.installed && mod.name !== currentModule && isConnected,
|
|
55
76
|
}"
|
|
56
|
-
:disabled="
|
|
77
|
+
:disabled="mod.name === currentModule"
|
|
57
78
|
@click="handleModuleClick(mod)"
|
|
58
79
|
>
|
|
59
80
|
<div class="splash-module-icon">
|
|
@@ -61,11 +82,26 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
61
82
|
</div>
|
|
62
83
|
<span class="splash-module-title">{{ mod.title }}</span>
|
|
63
84
|
<span v-if="mod.name === currentModule" class="splash-current-badge">Current</span>
|
|
64
|
-
<span v-else-if="!mod.installed" class="splash-not-installed">Not installed</span>
|
|
85
|
+
<span v-else-if="!mod.installed" class="splash-not-installed">{{ selectedForInstall.has(mod.name) ? 'Selected' : 'Not installed' }}</span>
|
|
65
86
|
</button>
|
|
66
87
|
</div>
|
|
67
88
|
</div>
|
|
68
89
|
|
|
90
|
+
<!-- Install command -->
|
|
91
|
+
<Transition name="install-bar">
|
|
92
|
+
<div v-if="installCommand" class="splash-install-section">
|
|
93
|
+
<div class="splash-install">
|
|
94
|
+
<div class="splash-install-code">
|
|
95
|
+
<code>{{ installCommand }}</code>
|
|
96
|
+
</div>
|
|
97
|
+
<button type="button" class="splash-install-copy" @click="copy()">
|
|
98
|
+
<UIcon :name="copied ? 'carbon:checkmark' : 'carbon:copy'" class="w-3.5 h-3.5" />
|
|
99
|
+
{{ copied ? 'Copied' : 'Copy' }}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</Transition>
|
|
104
|
+
|
|
69
105
|
<!-- Pro Section -->
|
|
70
106
|
<div v-if="proModules.length" class="splash-section splash-pro-area">
|
|
71
107
|
<div class="splash-section-label">
|
|
@@ -201,7 +237,7 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
.splash-section-dot--pro {
|
|
204
|
-
background:
|
|
240
|
+
background: oklch(65% 0.25 290);
|
|
205
241
|
}
|
|
206
242
|
|
|
207
243
|
/* Grid */
|
|
@@ -241,7 +277,29 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
241
277
|
}
|
|
242
278
|
|
|
243
279
|
.splash-module.is-unavailable {
|
|
244
|
-
opacity: 0.
|
|
280
|
+
opacity: 0.4;
|
|
281
|
+
cursor: pointer;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.splash-module.is-unavailable:hover {
|
|
285
|
+
opacity: 0.7;
|
|
286
|
+
background: var(--color-surface-elevated);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.splash-module.is-selected-install {
|
|
290
|
+
opacity: 1;
|
|
291
|
+
background: oklch(from var(--seo-green) l c h / 0.08);
|
|
292
|
+
cursor: pointer;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.splash-module.is-selected-install .splash-module-icon {
|
|
296
|
+
background: oklch(from var(--seo-green) l c h / 0.15);
|
|
297
|
+
color: var(--seo-green);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.splash-module.is-selected-install .splash-not-installed {
|
|
301
|
+
color: var(--seo-green);
|
|
302
|
+
font-weight: 600;
|
|
245
303
|
}
|
|
246
304
|
|
|
247
305
|
.splash-module-icon {
|
|
@@ -288,6 +346,8 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
288
346
|
.splash-pro-badge {
|
|
289
347
|
font-size: 9px !important;
|
|
290
348
|
flex-shrink: 0;
|
|
349
|
+
color: oklch(65% 0.25 290) !important;
|
|
350
|
+
border-color: oklch(65% 0.25 290 / 0.3) !important;
|
|
291
351
|
}
|
|
292
352
|
|
|
293
353
|
/* Pro status / CTA */
|
|
@@ -298,7 +358,8 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
298
358
|
margin: 0.375rem 0.25rem 0;
|
|
299
359
|
padding: 0.4rem 0.625rem;
|
|
300
360
|
border-radius: var(--radius-md);
|
|
301
|
-
background:
|
|
361
|
+
background: oklch(65% 0.25 290 / 0.06);
|
|
362
|
+
border: 1px solid oklch(65% 0.25 290 / 0.12);
|
|
302
363
|
font-size: 0.6875rem;
|
|
303
364
|
color: var(--color-text-muted);
|
|
304
365
|
}
|
|
@@ -315,7 +376,7 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
315
376
|
gap: 0.25rem;
|
|
316
377
|
font-size: 0.6875rem;
|
|
317
378
|
font-weight: 500;
|
|
318
|
-
color:
|
|
379
|
+
color: oklch(65% 0.25 290);
|
|
319
380
|
text-decoration: none;
|
|
320
381
|
transition: opacity 100ms;
|
|
321
382
|
}
|
|
@@ -324,6 +385,71 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
324
385
|
opacity: 0.8;
|
|
325
386
|
}
|
|
326
387
|
|
|
388
|
+
/* Install bar */
|
|
389
|
+
.splash-install-section {
|
|
390
|
+
padding: 0.5rem 0.75rem;
|
|
391
|
+
border-top: 1px solid var(--color-border);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.splash-install {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 0.5rem;
|
|
398
|
+
padding: 0.5rem 0.625rem;
|
|
399
|
+
border-radius: var(--radius-md);
|
|
400
|
+
background: var(--color-surface-sunken);
|
|
401
|
+
border: 1px solid oklch(from var(--seo-green) l c h / 0.2);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.splash-install-code {
|
|
405
|
+
flex: 1;
|
|
406
|
+
min-width: 0;
|
|
407
|
+
overflow-x: auto;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.splash-install-code code {
|
|
411
|
+
font-size: 0.6875rem;
|
|
412
|
+
font-family: var(--font-mono, monospace);
|
|
413
|
+
color: var(--color-text);
|
|
414
|
+
white-space: nowrap;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.splash-install-copy {
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
gap: 0.25rem;
|
|
421
|
+
flex-shrink: 0;
|
|
422
|
+
padding: 0.25rem 0.5rem;
|
|
423
|
+
border-radius: var(--radius-sm);
|
|
424
|
+
font-size: 0.625rem;
|
|
425
|
+
font-weight: 600;
|
|
426
|
+
color: var(--seo-green);
|
|
427
|
+
background: oklch(from var(--seo-green) l c h / 0.1);
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
transition: background 100ms;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.splash-install-copy:hover {
|
|
433
|
+
background: oklch(from var(--seo-green) l c h / 0.18);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.install-bar-enter-active,
|
|
437
|
+
.install-bar-leave-active {
|
|
438
|
+
transition: opacity 150ms ease, max-height 150ms ease;
|
|
439
|
+
overflow: hidden;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.install-bar-enter-from,
|
|
443
|
+
.install-bar-leave-to {
|
|
444
|
+
opacity: 0;
|
|
445
|
+
max-height: 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.install-bar-enter-to,
|
|
449
|
+
.install-bar-leave-from {
|
|
450
|
+
max-height: 4rem;
|
|
451
|
+
}
|
|
452
|
+
|
|
327
453
|
/* Footer */
|
|
328
454
|
.splash-footer {
|
|
329
455
|
display: flex;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
const {
|
|
3
3
|
title,
|
|
4
|
+
closable = false,
|
|
5
|
+
icon,
|
|
6
|
+
padding = true,
|
|
4
7
|
} = defineProps<{
|
|
5
8
|
title?: string
|
|
9
|
+
closable?: boolean
|
|
10
|
+
icon?: string
|
|
11
|
+
padding?: boolean
|
|
6
12
|
}>()
|
|
7
13
|
|
|
8
14
|
defineEmits<{
|
|
@@ -14,18 +20,22 @@ defineEmits<{
|
|
|
14
20
|
<div class="devtools-panel">
|
|
15
21
|
<div v-if="title || $slots.header" class="devtools-panel-header">
|
|
16
22
|
<slot name="header">
|
|
17
|
-
<
|
|
23
|
+
<div class="flex items-center gap-2">
|
|
24
|
+
<UIcon v-if="icon" :name="icon" class="text-sm text-[var(--color-text-muted)]" />
|
|
25
|
+
<span class="devtools-panel-title">{{ title }}</span>
|
|
26
|
+
</div>
|
|
18
27
|
</slot>
|
|
19
|
-
<div class="devtools-panel-actions">
|
|
28
|
+
<div v-if="closable || $slots.actions" class="devtools-panel-actions">
|
|
20
29
|
<slot name="actions" />
|
|
21
30
|
<UButton
|
|
31
|
+
v-if="closable"
|
|
22
32
|
icon="carbon:close"
|
|
23
33
|
aria-label="Close panel"
|
|
24
34
|
@click="$emit('close')"
|
|
25
35
|
/>
|
|
26
36
|
</div>
|
|
27
37
|
</div>
|
|
28
|
-
<div class="devtools-panel-content">
|
|
38
|
+
<div class="devtools-panel-content" :class="padding ? 'p-3' : ''">
|
|
29
39
|
<slot />
|
|
30
40
|
</div>
|
|
31
41
|
</div>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { moduleCatalog } from '../composables/modules'
|
|
4
|
+
|
|
5
|
+
const { moduleName } = defineProps<{
|
|
6
|
+
moduleName: string
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const keyLabels: Record<string, string> = {
|
|
10
|
+
'basic': 'Basic',
|
|
11
|
+
'i18n': 'Nuxt I18n',
|
|
12
|
+
'content': 'Nuxt Content',
|
|
13
|
+
'basic-satori': 'Basic (Satori)',
|
|
14
|
+
'basic-takumi': 'Basic (Takumi)',
|
|
15
|
+
'dynamic-urls': 'Dynamic URLs',
|
|
16
|
+
'custom-rules': 'Custom Rules',
|
|
17
|
+
'broken-links': 'Broken Links',
|
|
18
|
+
'skip-inspection': 'Skip Inspection',
|
|
19
|
+
'breadcrumbs': 'Breadcrumbs',
|
|
20
|
+
'meta-tags': 'Meta Tags',
|
|
21
|
+
'blog': 'Blog',
|
|
22
|
+
'e-commerce': 'E-Commerce',
|
|
23
|
+
'env-driven': 'Env Driven',
|
|
24
|
+
'multi-site': 'Multi Site',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CAMEL_RE = /([a-z])([A-Z])/g
|
|
28
|
+
const SEP_RE = /[-_]/g
|
|
29
|
+
const WORD_RE = /\b\w/g
|
|
30
|
+
|
|
31
|
+
function humanizeKey(key: string) {
|
|
32
|
+
if (keyLabels[key])
|
|
33
|
+
return keyLabels[key]
|
|
34
|
+
return key
|
|
35
|
+
.replace(CAMEL_RE, '$1 $2')
|
|
36
|
+
.replace(SEP_RE, ' ')
|
|
37
|
+
.replace(WORD_RE, c => c.toUpperCase())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const playgrounds = computed(() => {
|
|
41
|
+
const mod = moduleCatalog.value.find(m => m.name === moduleName)
|
|
42
|
+
if (!mod?.playgrounds)
|
|
43
|
+
return []
|
|
44
|
+
return Object.entries(mod.playgrounds).map(([key, url]) => ({
|
|
45
|
+
key,
|
|
46
|
+
label: humanizeKey(key),
|
|
47
|
+
url,
|
|
48
|
+
}))
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div v-if="playgrounds.length" class="playgrounds-list">
|
|
54
|
+
<a
|
|
55
|
+
v-for="pg in playgrounds"
|
|
56
|
+
:key="pg.key"
|
|
57
|
+
:href="pg.url"
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener"
|
|
60
|
+
class="playground-btn"
|
|
61
|
+
>
|
|
62
|
+
<UIcon name="carbon:launch" class="w-3 h-3 shrink-0 opacity-50" />
|
|
63
|
+
<span>{{ pg.label }}</span>
|
|
64
|
+
</a>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.playgrounds-list {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-wrap: wrap;
|
|
72
|
+
gap: 0.375rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.playground-btn {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: 0.375rem;
|
|
79
|
+
padding: 0.25rem 0.625rem;
|
|
80
|
+
border-radius: var(--radius-sm);
|
|
81
|
+
font-size: 0.75rem;
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
color: var(--color-text-muted);
|
|
84
|
+
background: var(--color-surface-elevated);
|
|
85
|
+
border: 1px solid var(--color-border);
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
transition: background 100ms, color 100ms, border-color 100ms;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.playground-btn:hover {
|
|
91
|
+
background: var(--color-surface-sunken);
|
|
92
|
+
color: var(--color-text);
|
|
93
|
+
border-color: var(--color-neutral-400);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.dark .playground-btn:hover {
|
|
97
|
+
border-color: var(--color-neutral-600);
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useClipboard } from '@vueuse/core'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { installedModules, moduleCatalog } from '../composables/modules'
|
|
5
|
+
import { detectPackageManager, pmCommands } from '../composables/package-manager'
|
|
6
|
+
import { useModuleUpdate } from '../composables/update-check'
|
|
7
|
+
|
|
8
|
+
const { moduleName, version } = defineProps<{
|
|
9
|
+
moduleName: string
|
|
10
|
+
version?: string
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
detectPackageManager()
|
|
14
|
+
|
|
15
|
+
const nuxtApp = useNuxtApp()
|
|
16
|
+
const nuxtVersion = nuxtApp.versions?.nuxt ?? 'unknown'
|
|
17
|
+
const vueVersion = nuxtApp.versions?.vue ?? 'unknown'
|
|
18
|
+
|
|
19
|
+
const moduleInfo = computed(() => moduleCatalog.value.find(m => m.name === moduleName))
|
|
20
|
+
const npmPackage = computed(() => moduleInfo.value?.npm)
|
|
21
|
+
const { latestVersion, hasUpdate } = useModuleUpdate(npmPackage.value, version)
|
|
22
|
+
|
|
23
|
+
const githubNewIssueUrl = computed(() => {
|
|
24
|
+
if (!moduleInfo.value?.repo)
|
|
25
|
+
return ''
|
|
26
|
+
return `https://github.com/${moduleInfo.value.repo}/issues/new`
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Build environment info for copy
|
|
30
|
+
const envInfo = computed(() => {
|
|
31
|
+
const lines: string[] = []
|
|
32
|
+
lines.push('### Environment')
|
|
33
|
+
lines.push('')
|
|
34
|
+
|
|
35
|
+
// Current module
|
|
36
|
+
if (version)
|
|
37
|
+
lines.push(`- **${npmPackage.value}**: v${version}${hasUpdate.value ? ` (latest: v${latestVersion.value})` : ''}`)
|
|
38
|
+
|
|
39
|
+
// Other installed Nuxt SEO modules
|
|
40
|
+
for (const mod of installedModules.value) {
|
|
41
|
+
if (mod.name === moduleName)
|
|
42
|
+
continue
|
|
43
|
+
const catalogEntry = moduleCatalog.value.find(m => m.name === mod.name)
|
|
44
|
+
if (catalogEntry?.npm)
|
|
45
|
+
lines.push(`- **${catalogEntry.npm}**: installed`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines.push('')
|
|
49
|
+
lines.push(`- **Nuxt**: v${nuxtVersion}`)
|
|
50
|
+
lines.push(`- **Vue**: v${vueVersion}`)
|
|
51
|
+
|
|
52
|
+
return lines.join('\n')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const { copy: copyEnv, copied: envCopied } = useClipboard({ source: envInfo })
|
|
56
|
+
|
|
57
|
+
const steps = computed(() => {
|
|
58
|
+
const pm = pmCommands()
|
|
59
|
+
const updateCmd = [pm.update, pm.dedupe].filter(Boolean).join(' && ')
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
icon: 'carbon:reset',
|
|
63
|
+
title: 'Clear generated files and update dependencies',
|
|
64
|
+
description: 'Delete your .nuxt directory, update packages, then restart.',
|
|
65
|
+
codes: [
|
|
66
|
+
`rm -rf .nuxt && ${pm.exec} nuxi dev`,
|
|
67
|
+
updateCmd,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
icon: 'carbon:debug',
|
|
72
|
+
title: 'Enable debug mode',
|
|
73
|
+
description: 'Add debug: true to your module config for verbose logging.',
|
|
74
|
+
codes: [],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
icon: 'carbon:code',
|
|
78
|
+
title: 'Create a minimal reproduction',
|
|
79
|
+
description: 'Use a StackBlitz playground to isolate the issue. This helps maintainers debug quickly.',
|
|
80
|
+
codes: [],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
icon: 'carbon:flag',
|
|
84
|
+
title: 'Report the issue',
|
|
85
|
+
description: 'Open a GitHub issue with your reproduction link and environment info.',
|
|
86
|
+
codes: [],
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<DevtoolsSection icon="carbon:help" text="Troubleshooting" description="Steps to diagnose and report issues">
|
|
94
|
+
<div class="troubleshoot-steps">
|
|
95
|
+
<div v-for="(step, i) of steps" :key="i" class="troubleshoot-step">
|
|
96
|
+
<div class="troubleshoot-step-num">
|
|
97
|
+
{{ i + 1 }}
|
|
98
|
+
</div>
|
|
99
|
+
<div class="troubleshoot-step-content">
|
|
100
|
+
<div class="troubleshoot-step-title">
|
|
101
|
+
<UIcon :name="step.icon" class="w-3.5 h-3.5 text-[var(--color-text-muted)]" />
|
|
102
|
+
{{ step.title }}
|
|
103
|
+
</div>
|
|
104
|
+
<div class="troubleshoot-step-desc">
|
|
105
|
+
{{ step.description }}
|
|
106
|
+
</div>
|
|
107
|
+
<code v-for="code of step.codes" :key="code" class="troubleshoot-code">{{ code }}</code>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Playgrounds for repro -->
|
|
113
|
+
<div class="troubleshoot-section">
|
|
114
|
+
<div class="troubleshoot-section-label">
|
|
115
|
+
<UIcon name="carbon:game-console" class="w-3.5 h-3.5" />
|
|
116
|
+
Playgrounds for reproduction
|
|
117
|
+
</div>
|
|
118
|
+
<DevtoolsPlaygrounds :module-name="moduleName" />
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Environment info -->
|
|
122
|
+
<div class="troubleshoot-section">
|
|
123
|
+
<div class="troubleshoot-section-header">
|
|
124
|
+
<div class="troubleshoot-section-label">
|
|
125
|
+
<UIcon name="carbon:information" class="w-3.5 h-3.5" />
|
|
126
|
+
Environment info
|
|
127
|
+
</div>
|
|
128
|
+
<button type="button" class="troubleshoot-copy-btn" @click="copyEnv()">
|
|
129
|
+
<UIcon :name="envCopied ? 'carbon:checkmark' : 'carbon:copy'" class="w-3 h-3" />
|
|
130
|
+
{{ envCopied ? 'Copied' : 'Copy for issue' }}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="troubleshoot-env">
|
|
134
|
+
<div class="troubleshoot-env-row">
|
|
135
|
+
<span class="troubleshoot-env-label">{{ npmPackage }}</span>
|
|
136
|
+
<span class="troubleshoot-env-value">
|
|
137
|
+
v{{ version || 'unknown' }}
|
|
138
|
+
<UBadge v-if="hasUpdate" size="xs" color="warning" variant="subtle">
|
|
139
|
+
v{{ latestVersion }} available
|
|
140
|
+
</UBadge>
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
<template v-for="mod of installedModules" :key="mod.name">
|
|
144
|
+
<div v-if="mod.name !== moduleName" class="troubleshoot-env-row">
|
|
145
|
+
<span class="troubleshoot-env-label">{{ moduleCatalog.find(m => m.name === mod.name)?.npm || mod.name }}</span>
|
|
146
|
+
<span class="troubleshoot-env-value">installed</span>
|
|
147
|
+
</div>
|
|
148
|
+
</template>
|
|
149
|
+
<div class="troubleshoot-env-row">
|
|
150
|
+
<span class="troubleshoot-env-label">nuxt</span>
|
|
151
|
+
<span class="troubleshoot-env-value">v{{ nuxtVersion }}</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="troubleshoot-env-row">
|
|
154
|
+
<span class="troubleshoot-env-label">vue</span>
|
|
155
|
+
<span class="troubleshoot-env-value">v{{ vueVersion }}</span>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Report button -->
|
|
161
|
+
<a
|
|
162
|
+
v-if="githubNewIssueUrl"
|
|
163
|
+
:href="githubNewIssueUrl"
|
|
164
|
+
target="_blank"
|
|
165
|
+
rel="noopener"
|
|
166
|
+
class="troubleshoot-report-btn"
|
|
167
|
+
>
|
|
168
|
+
<UIcon name="simple-icons:github" class="w-3.5 h-3.5" />
|
|
169
|
+
Open an issue on GitHub
|
|
170
|
+
<UIcon name="carbon:arrow-up-right" class="w-3 h-3 opacity-40 ml-auto" />
|
|
171
|
+
</a>
|
|
172
|
+
</DevtoolsSection>
|
|
173
|
+
</template>
|
|
174
|
+
|
|
175
|
+
<style scoped>
|
|
176
|
+
.troubleshoot-steps {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
gap: 0.125rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.troubleshoot-step {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: flex-start;
|
|
185
|
+
gap: 0.75rem;
|
|
186
|
+
padding: 0.5rem 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.troubleshoot-step-num {
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
justify-content: center;
|
|
193
|
+
width: 1.375rem;
|
|
194
|
+
height: 1.375rem;
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
border-radius: 50%;
|
|
197
|
+
background: var(--color-surface-elevated);
|
|
198
|
+
border: 1px solid var(--color-border);
|
|
199
|
+
font-size: 0.625rem;
|
|
200
|
+
font-weight: 700;
|
|
201
|
+
color: var(--color-text-muted);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.troubleshoot-step-content {
|
|
205
|
+
flex: 1;
|
|
206
|
+
min-width: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.troubleshoot-step-title {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 0.375rem;
|
|
213
|
+
font-size: 0.8125rem;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
color: var(--color-text);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.troubleshoot-step-desc {
|
|
219
|
+
font-size: 0.75rem;
|
|
220
|
+
color: var(--color-text-muted);
|
|
221
|
+
margin-top: 0.125rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.troubleshoot-code {
|
|
225
|
+
display: inline-block;
|
|
226
|
+
margin-top: 0.375rem;
|
|
227
|
+
margin-right: 0.375rem;
|
|
228
|
+
padding: 0.25rem 0.5rem;
|
|
229
|
+
border-radius: var(--radius-sm);
|
|
230
|
+
background: var(--color-surface-elevated);
|
|
231
|
+
border: 1px solid var(--color-border);
|
|
232
|
+
font-family: var(--font-mono, monospace);
|
|
233
|
+
font-size: 0.6875rem;
|
|
234
|
+
color: var(--color-text);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.troubleshoot-section {
|
|
238
|
+
margin-top: 0.5rem;
|
|
239
|
+
padding-top: 0.75rem;
|
|
240
|
+
border-top: 1px solid var(--color-border);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.troubleshoot-section-header {
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
justify-content: space-between;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.troubleshoot-section-label {
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
gap: 0.375rem;
|
|
253
|
+
font-size: 0.75rem;
|
|
254
|
+
font-weight: 600;
|
|
255
|
+
color: var(--color-text-muted);
|
|
256
|
+
margin-bottom: 0.5rem;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.troubleshoot-copy-btn {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 0.25rem;
|
|
263
|
+
padding: 0.2rem 0.5rem;
|
|
264
|
+
border-radius: var(--radius-sm);
|
|
265
|
+
font-size: 0.625rem;
|
|
266
|
+
font-weight: 600;
|
|
267
|
+
color: var(--seo-green);
|
|
268
|
+
background: oklch(from var(--seo-green) l c h / 0.1);
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
transition: background 100ms;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.troubleshoot-copy-btn:hover {
|
|
274
|
+
background: oklch(from var(--seo-green) l c h / 0.18);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.troubleshoot-env {
|
|
278
|
+
display: flex;
|
|
279
|
+
flex-direction: column;
|
|
280
|
+
gap: 1px;
|
|
281
|
+
border-radius: var(--radius-sm);
|
|
282
|
+
overflow: hidden;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.troubleshoot-env-row {
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
justify-content: space-between;
|
|
289
|
+
padding: 0.3rem 0.5rem;
|
|
290
|
+
background: var(--color-surface-elevated);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.troubleshoot-env-label {
|
|
294
|
+
font-size: 0.6875rem;
|
|
295
|
+
font-family: var(--font-mono, monospace);
|
|
296
|
+
color: var(--color-text-muted);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.troubleshoot-env-value {
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
gap: 0.375rem;
|
|
303
|
+
font-size: 0.6875rem;
|
|
304
|
+
font-family: var(--font-mono, monospace);
|
|
305
|
+
color: var(--color-text);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.troubleshoot-report-btn {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 0.5rem;
|
|
312
|
+
margin-top: 0.75rem;
|
|
313
|
+
padding: 0.5rem 0.625rem;
|
|
314
|
+
border-radius: var(--radius-sm);
|
|
315
|
+
font-size: 0.8125rem;
|
|
316
|
+
font-weight: 500;
|
|
317
|
+
color: var(--color-text-muted);
|
|
318
|
+
text-decoration: none;
|
|
319
|
+
border: 1px solid var(--color-border);
|
|
320
|
+
transition: background 100ms, color 100ms, border-color 100ms;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.troubleshoot-report-btn:hover {
|
|
324
|
+
background: var(--color-surface-elevated);
|
|
325
|
+
color: var(--color-text);
|
|
326
|
+
border-color: var(--color-neutral-400);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.dark .troubleshoot-report-btn:hover {
|
|
330
|
+
border-color: var(--color-neutral-600);
|
|
331
|
+
}
|
|
332
|
+
</style>
|
package/composables/modules.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NuxtDevtoolsIframeClient } from '@nuxt/devtools-kit/types'
|
|
2
|
+
import type { NuxtSEOModule } from 'nuxtseo-shared/const'
|
|
2
3
|
import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
|
|
4
|
+
import { modules as seoModules } from 'nuxtseo-shared/const'
|
|
3
5
|
import { computed, ref } from 'vue'
|
|
4
6
|
import { isConnected } from './state'
|
|
5
7
|
|
|
@@ -17,22 +19,51 @@ export interface SeoModuleCatalogEntry {
|
|
|
17
19
|
icon: string
|
|
18
20
|
installed: boolean
|
|
19
21
|
route?: string
|
|
20
|
-
|
|
22
|
+
npm: string
|
|
23
|
+
repo: string
|
|
21
24
|
pro?: boolean
|
|
25
|
+
playgrounds?: Record<string, string>
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
// Map slug to the internal module name used by devtools routing
|
|
29
|
+
const SLUG_TO_MODULE_NAME: Record<string, string> = {
|
|
30
|
+
'robots': 'nuxt-robots',
|
|
31
|
+
'sitemap': 'sitemap',
|
|
32
|
+
'og-image': 'nuxt-og-image',
|
|
33
|
+
'schema-org': 'nuxt-schema-org',
|
|
34
|
+
'seo-utils': 'nuxt-seo-utils',
|
|
35
|
+
'link-checker': 'nuxt-link-checker',
|
|
36
|
+
'site-config': 'nuxt-site-config',
|
|
37
|
+
'ai-ready': 'nuxt-ai-ready',
|
|
38
|
+
'skew-protection': 'nuxt-skew-protection',
|
|
39
|
+
'ai-kit': 'nuxt-ai-kit',
|
|
40
|
+
'nuxt-seo': 'nuxt-seo',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ICONIFY_RE = /^i-([^-]+)-/
|
|
44
|
+
|
|
45
|
+
function toIconify(icon: string): string {
|
|
46
|
+
// Convert i-carbon-bot -> carbon:bot
|
|
47
|
+
return icon.replace(ICONIFY_RE, '$1:')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function moduleToCatalogEntry(mod: NuxtSEOModule): Omit<SeoModuleCatalogEntry, 'installed' | 'route'> {
|
|
51
|
+
return {
|
|
52
|
+
name: SLUG_TO_MODULE_NAME[mod.slug] || mod.slug,
|
|
53
|
+
title: mod.label,
|
|
54
|
+
description: mod.description,
|
|
55
|
+
icon: toIconify(mod.icon),
|
|
56
|
+
npm: mod.npm,
|
|
57
|
+
repo: mod.repo,
|
|
58
|
+
pro: mod.pro,
|
|
59
|
+
playgrounds: mod.playgrounds,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Exclude the meta 'nuxt-seo' entry from the catalog, it's not a standalone devtools module
|
|
64
|
+
const MODULE_CATALOG = seoModules
|
|
65
|
+
.filter(m => m.slug !== 'nuxt-seo')
|
|
66
|
+
.map(moduleToCatalogEntry)
|
|
36
67
|
|
|
37
68
|
export const installedModules = ref<SeoModuleInfo[]>([])
|
|
38
69
|
export const showModuleSplash = ref(false)
|
|
@@ -48,6 +79,10 @@ export const moduleCatalog = computed<SeoModuleCatalogEntry[]>(() => {
|
|
|
48
79
|
})
|
|
49
80
|
})
|
|
50
81
|
|
|
82
|
+
export function findModuleByName(moduleName: string): SeoModuleCatalogEntry | undefined {
|
|
83
|
+
return moduleCatalog.value.find(m => m.name === moduleName)
|
|
84
|
+
}
|
|
85
|
+
|
|
51
86
|
export function fetchInstalledModules(): void {
|
|
52
87
|
const inIframe = window.parent !== window
|
|
53
88
|
if (!inIframe)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm'
|
|
4
|
+
|
|
5
|
+
export const packageManager = ref<PackageManager>('npm')
|
|
6
|
+
|
|
7
|
+
const PM_COMMANDS: Record<PackageManager, { run: string, exec: string, update: string, dedupe: string }> = {
|
|
8
|
+
pnpm: { run: 'pnpm', exec: 'pnpm dlx', update: 'pnpm update', dedupe: 'pnpm dedupe' },
|
|
9
|
+
yarn: { run: 'yarn', exec: 'yarn dlx', update: 'yarn upgrade', dedupe: 'yarn dedupe' },
|
|
10
|
+
bun: { run: 'bun', exec: 'bunx', update: 'bun update', dedupe: '' },
|
|
11
|
+
npm: { run: 'npm', exec: 'npx', update: 'npm update', dedupe: 'npm dedupe' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function pmCommands() {
|
|
15
|
+
return PM_COMMANDS[packageManager.value]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Detect by checking lock files via Vite's @fs route (dev only)
|
|
19
|
+
const LOCK_FILES: [string, PackageManager][] = [
|
|
20
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
21
|
+
['yarn.lock', 'yarn'],
|
|
22
|
+
['bun.lockb', 'bun'],
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
let detected = false
|
|
26
|
+
export function detectPackageManager() {
|
|
27
|
+
if (detected)
|
|
28
|
+
return
|
|
29
|
+
detected = true
|
|
30
|
+
|
|
31
|
+
// Check each lock file with a HEAD request
|
|
32
|
+
for (const [file, pm] of LOCK_FILES) {
|
|
33
|
+
fetch(`/${file}`, { method: 'HEAD' })
|
|
34
|
+
.then((r) => {
|
|
35
|
+
if (r.ok) {
|
|
36
|
+
packageManager.value = pm
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.catch(() => {})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ModuleUpdateInfo {
|
|
5
|
+
currentVersion: string
|
|
6
|
+
latestVersion: string
|
|
7
|
+
hasUpdate: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const updateCache = ref<Record<string, ModuleUpdateInfo>>({})
|
|
11
|
+
|
|
12
|
+
export function useModuleUpdate(npmPackage: string | undefined, currentVersion: string | undefined): { hasUpdate: ComputedRef<boolean>, latestVersion: ComputedRef<string | undefined>, info: ComputedRef<ModuleUpdateInfo | undefined> } {
|
|
13
|
+
const info = computed(() => {
|
|
14
|
+
if (!npmPackage || !currentVersion)
|
|
15
|
+
return undefined
|
|
16
|
+
return updateCache.value[npmPackage]
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const hasUpdate = computed(() => info.value?.hasUpdate ?? false)
|
|
20
|
+
const latestVersion = computed(() => info.value?.latestVersion)
|
|
21
|
+
|
|
22
|
+
if (npmPackage && currentVersion && !updateCache.value[npmPackage]) {
|
|
23
|
+
fetch(`https://registry.npmjs.org/${npmPackage}/latest`)
|
|
24
|
+
.then(r => r.json())
|
|
25
|
+
.then((data) => {
|
|
26
|
+
const latest = data.version as string
|
|
27
|
+
if (!latest)
|
|
28
|
+
return
|
|
29
|
+
updateCache.value[npmPackage] = {
|
|
30
|
+
currentVersion,
|
|
31
|
+
latestVersion: latest,
|
|
32
|
+
hasUpdate: latest !== currentVersion,
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
.catch(() => {})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { hasUpdate, latestVersion, info }
|
|
39
|
+
}
|
package/error.vue
CHANGED
|
@@ -5,11 +5,14 @@ const { error } = defineProps<{
|
|
|
5
5
|
error: NuxtError
|
|
6
6
|
}>()
|
|
7
7
|
|
|
8
|
+
// eslint-disable-next-line no-control-regex
|
|
9
|
+
const ANSI_RE = /\u001B\[[0-9;]*m/g
|
|
10
|
+
|
|
8
11
|
const stack = computed(() => {
|
|
9
12
|
if (!error.stack)
|
|
10
13
|
return ''
|
|
11
14
|
// Clean ANSI codes if present
|
|
12
|
-
return error.stack.replace(
|
|
15
|
+
return error.stack.replace(ANSI_RE, '')
|
|
13
16
|
})
|
|
14
17
|
|
|
15
18
|
function handleClear() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxtseo-layer-devtools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Shared Nuxt layer for Nuxt SEO devtools clients.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"@vueuse/nuxt": "^14.2.1",
|
|
37
37
|
"ofetch": "^1.5.1",
|
|
38
38
|
"shiki": "^4.0.2",
|
|
39
|
-
"ufo": "^1.6.3"
|
|
39
|
+
"ufo": "^1.6.3",
|
|
40
|
+
"nuxtseo-shared": "0.8.2"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"nuxt": "^4.4.2",
|
|
@@ -9,6 +9,14 @@ Shared Nuxt layer providing components, composables, and a design system for all
|
|
|
9
9
|
|
|
10
10
|
**Source:** `packages/devtools-layer/` (published as `nuxtseo-layer-devtools`)
|
|
11
11
|
|
|
12
|
+
## Available Libraries
|
|
13
|
+
|
|
14
|
+
The layer registers these Nuxt modules, so all consumers have them available without extra config:
|
|
15
|
+
|
|
16
|
+
- **`@nuxt/ui`** (v4): Full component library. Use `UButton`, `UBadge`, `UIcon`, `UInput`, `UTooltip`, `UApp`, etc. freely in devtools clients. Default variants configured via `app.config.ts` (primary: green, buttons: ghost/neutral/sm, badges: subtle/neutral/xs, tooltips: zero delay).
|
|
17
|
+
- **`@vueuse/nuxt`**: All VueUse composables auto imported.
|
|
18
|
+
- **Shiki**: Syntax highlighting via the layer's `loadShiki` / `useRenderCodeHighlight` composables.
|
|
19
|
+
|
|
12
20
|
## Architecture
|
|
13
21
|
|
|
14
22
|
1. **nuxtseo-shared/devtools** (`packages/shared/src/devtools.ts`): `setupDevToolsUI()` registers iframe tab, handles dev proxy + production sirv
|
|
@@ -20,13 +28,14 @@ Shared Nuxt layer providing components, composables, and a design system for all
|
|
|
20
28
|
1. ALWAYS extend `nuxtseo-layer-devtools` in client/nuxt.config.ts
|
|
21
29
|
2. ALWAYS create `client/composables/rpc.ts` that calls `useDevtoolsConnection()`
|
|
22
30
|
3. ALWAYS use layer components over custom HTML: `DevtoolsSection` not custom details, `DevtoolsKeyValue` not custom tables, `DevtoolsSnippet` not custom code blocks. Use `KeyValueItem.code` for inline code rendering instead of separate snippets
|
|
23
|
-
4.
|
|
24
|
-
5. NEVER
|
|
25
|
-
6.
|
|
26
|
-
7. ALWAYS
|
|
27
|
-
8. ALWAYS
|
|
28
|
-
9.
|
|
29
|
-
10.
|
|
31
|
+
4. ALWAYS use `@nuxt/ui` components (`UButton`, `UInput`, `UBadge`, `UIcon`, `UTooltip`, etc.) for interactive elements. Never add a custom button, input, badge, or tooltip when a Nuxt UI component exists.
|
|
32
|
+
5. NEVER add custom CSS that duplicates what the layer or Nuxt UI provides
|
|
33
|
+
6. NEVER enable SSR in the client (it runs in an iframe)
|
|
34
|
+
7. ALWAYS disable the module itself in the client nuxt config (e.g. `robots: false`)
|
|
35
|
+
8. ALWAYS guard devtools setup with `if (nuxt.options.dev)` in module.ts
|
|
36
|
+
9. ALWAYS use `useAsyncData` with `watch: [refreshTime]` for reactive data fetching
|
|
37
|
+
10. Use Carbon icons consistently (prefix: `carbon:`)
|
|
38
|
+
11. Debug server routes ONLY registered in dev mode
|
|
30
39
|
|
|
31
40
|
## Required File Structure
|
|
32
41
|
|
|
@@ -99,7 +108,7 @@ watch(data, () => {
|
|
|
99
108
|
</script>
|
|
100
109
|
|
|
101
110
|
<template>
|
|
102
|
-
<DevtoolsLayout v-model:active-tab="activeTab" title="Name" icon="carbon:icon" :nav-items="navItems" github-url="..." :loading @refresh="refreshSources">
|
|
111
|
+
<DevtoolsLayout v-model:active-tab="activeTab" title="Name" icon="carbon:icon" module-name="nuxt-<module>" :nav-items="navItems" github-url="..." :loading @refresh="refreshSources">
|
|
103
112
|
<DevtoolsLoading v-if="loading" />
|
|
104
113
|
<template v-else-if="activeTab === 'overview'">
|
|
105
114
|
<!-- content -->
|
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
# Devtools Layer API Reference
|
|
2
2
|
|
|
3
|
+
## Nuxt UI (available to all consumers)
|
|
4
|
+
|
|
5
|
+
The layer registers `@nuxt/ui` v4, so all Nuxt UI components are auto imported. Use them freely:
|
|
6
|
+
|
|
7
|
+
- `UButton`, `UInput`, `UTextarea`, `USelect`, `UCheckbox`, `UToggle`, `URadio`
|
|
8
|
+
- `UBadge`, `UIcon`, `UTooltip`, `UPopover`, `UModal`, `UDrawer`
|
|
9
|
+
- `UApp` (wraps the root, already used by `DevtoolsLayout`)
|
|
10
|
+
- `UCard`, `UAccordion`, `UTabs`, `UDropdownMenu`
|
|
11
|
+
|
|
12
|
+
Default variants via `app.config.ts`: primary green, neutral neutral. Buttons: ghost/neutral/sm. Badges: subtle/neutral/xs. Tooltips: zero delay.
|
|
13
|
+
|
|
14
|
+
Icons use the Iconify format. Convention: `carbon:*` prefix for consistency.
|
|
15
|
+
|
|
3
16
|
## Components (auto imported)
|
|
4
17
|
|
|
5
18
|
### Layout & Structure
|
|
6
19
|
|
|
7
20
|
| Component | Props | Key Slots | Purpose |
|
|
8
21
|
|---|---|---|---|
|
|
9
|
-
| `DevtoolsLayout` | `title`, `icon`, `version?`, `navItems`, `githubUrl`, `loading?` | `actions`, default | Main shell with header, tabs, refresh |
|
|
10
|
-
| `DevtoolsPanel` | `title?` | `header`, `actions`, default | Card container
|
|
22
|
+
| `DevtoolsLayout` | `title`, `icon`, `version?`, `moduleName?`, `npmPackage?`, `navItems: DevtoolsNavItem[]`, `githubUrl`, `loading?` | `actions`, default | Main shell with header, tabs, refresh, module splash, standalone mode, production mode. Pass `npmPackage` to enable update check indicator on version badge. |
|
|
23
|
+
| `DevtoolsPanel` | `title?`, `icon?`, `closable?` (false), `padding?` (true) | `header`, `actions`, default | Card container. Emits `close` when closable button clicked. |
|
|
11
24
|
| `DevtoolsToolbar` | `variant` ('default'\|'minimal') | default | Horizontal toolbar strip |
|
|
12
25
|
| `DevtoolsSection` | `icon?`, `text?`, `description?`, `collapse?`, `open?`, `padding?` | `text`, `description`, `actions`, `details`, default, `footer` | Collapsible details/summary block |
|
|
13
26
|
|
|
27
|
+
### DevtoolsNavItem Interface
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
interface DevtoolsNavItem {
|
|
31
|
+
value: string
|
|
32
|
+
to?: string // If set, renders as NuxtLink (route navigation)
|
|
33
|
+
icon: string
|
|
34
|
+
label: string
|
|
35
|
+
devOnly?: boolean // Hidden in production mode
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
14
39
|
### Feedback & States
|
|
15
40
|
|
|
16
41
|
| Component | Props | Key Slots | Purpose |
|
|
@@ -31,6 +56,16 @@
|
|
|
31
56
|
| `DevtoolsSnippet` | `label?`, `code`, `lang` ('js'\|'json'\|'xml') | `header` | Code block with header, copy, max 300px scroll |
|
|
32
57
|
| `OCodeBlock` | `code`, `lang`, `lines?`, `transformRendered?` | none | Shiki syntax highlighted pre |
|
|
33
58
|
| `DevtoolsDocs` | `url` | none | Full height iframe |
|
|
59
|
+
| `DevtoolsPlaygrounds` | `moduleName` | none | StackBlitz playground links with UTabs for variant selection. Reusable standalone or inside other components. Shows nothing if module has no playgrounds. |
|
|
60
|
+
| `DevtoolsTroubleshooting` | `moduleName`, `version?` | none | Guided troubleshooting section: clear .nuxt, debug mode, create repro (with playgrounds), report issue. Shows all installed module versions with copy for GitHub issues. |
|
|
61
|
+
|
|
62
|
+
### Module Navigation
|
|
63
|
+
|
|
64
|
+
| Component | Props | Key Slots | Purpose |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `DevtoolsModuleSplash` | `currentModule?` | none | Modal overlay showing all Nuxt SEO modules with install status, switch between modules, Pro section |
|
|
67
|
+
| `DevtoolsStandaloneConnect` | none | none | Connection form for standalone mode (outside devtools iframe) |
|
|
68
|
+
| `NuxtSeoLogo` | none | none | Nuxt SEO brand logo SVG |
|
|
34
69
|
|
|
35
70
|
### KeyValueItem Interface
|
|
36
71
|
|
|
@@ -73,6 +108,11 @@ const host: ComputedRef<string>
|
|
|
73
108
|
const refreshSources: () => void // Debounced 200ms
|
|
74
109
|
const slowRefreshSources: () => void // Debounced 1000ms
|
|
75
110
|
|
|
111
|
+
// Standalone mode (running outside devtools iframe)
|
|
112
|
+
const standaloneUrl: Ref<string> // localStorage persisted
|
|
113
|
+
const isConnected: Ref<boolean>
|
|
114
|
+
const isStandalone: ComputedRef<boolean> // true when not connected but has standaloneUrl
|
|
115
|
+
|
|
76
116
|
// Production preview mode
|
|
77
117
|
const previewSource: Ref<'local' | 'production'> // localStorage persisted
|
|
78
118
|
const productionUrl: Ref<string>
|
|
@@ -80,6 +120,21 @@ const hasProductionUrl: ComputedRef<boolean>
|
|
|
80
120
|
const isProductionMode: ComputedRef<boolean>
|
|
81
121
|
```
|
|
82
122
|
|
|
123
|
+
### Modules (`composables/modules.ts`)
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
interface SeoModuleInfo { name: string, title: string, icon: string, route: string }
|
|
127
|
+
interface SeoModuleCatalogEntry extends SeoModuleInfo { description: string, installed: boolean, npm: string, pro?: boolean, playgrounds?: Record<string, string> }
|
|
128
|
+
|
|
129
|
+
const installedModules: Ref<SeoModuleInfo[]>
|
|
130
|
+
const showModuleSplash: Ref<boolean>
|
|
131
|
+
const moduleCatalog: ComputedRef<SeoModuleCatalogEntry[]>
|
|
132
|
+
|
|
133
|
+
function fetchInstalledModules(): void // Called by DevtoolsLayout automatically
|
|
134
|
+
function findModuleByName(moduleName: string): SeoModuleCatalogEntry | undefined // Look up module info by name
|
|
135
|
+
function switchToModule(moduleName: string): void // Navigate devtools iframe to another module
|
|
136
|
+
```
|
|
137
|
+
|
|
83
138
|
### Shiki (`composables/shiki.ts`)
|
|
84
139
|
|
|
85
140
|
```ts
|
|
@@ -93,9 +148,21 @@ function useRenderCodeHighlight(code: MaybeRef<string>, lang: string): ComputedR
|
|
|
93
148
|
function useCopy(timeout?: number): { copy: (text: string) => Promise<void>, copied: Ref<boolean> }
|
|
94
149
|
```
|
|
95
150
|
|
|
151
|
+
### Update Check (`composables/update-check.ts`)
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
function useModuleUpdate(npmPackage: string | undefined, currentVersion: string | undefined): {
|
|
155
|
+
hasUpdate: ComputedRef<boolean>
|
|
156
|
+
latestVersion: ComputedRef<string | undefined>
|
|
157
|
+
info: ComputedRef<ModuleUpdateInfo | undefined>
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Fetches latest version from npm registry. Results are cached per package name. Already integrated into `DevtoolsLayout` when `npmPackage` prop is provided.
|
|
162
|
+
|
|
96
163
|
## CSS Design System
|
|
97
164
|
|
|
98
|
-
Do NOT add custom CSS unless layer components are insufficient.
|
|
165
|
+
Do NOT add custom CSS unless layer components or Nuxt UI are insufficient.
|
|
99
166
|
|
|
100
167
|
### Semantic Variables
|
|
101
168
|
|
|
@@ -105,10 +172,6 @@ Do NOT add custom CSS unless layer components are insufficient.
|
|
|
105
172
|
|
|
106
173
|
`.glass` (backdrop blur), `.gradient-bg` (green/blue radials), `.card` (elevated hover), `.code-block`, `.status-enabled/.status-disabled`, `.link-external` (with arrow), `.hint-callout`, `.panel-grids`, `.animate-fade-up/.scale-in/.spin`, `.stagger-children`, `.devtools-main-content` (max-width 80rem centered container)
|
|
107
174
|
|
|
108
|
-
### Nuxt UI Defaults (app.config.ts)
|
|
109
|
-
|
|
110
|
-
Primary: green, Neutral: neutral. Buttons: ghost/neutral/sm. Badges: subtle/neutral/xs. Tooltips: zero delay.
|
|
111
|
-
|
|
112
175
|
## Common Patterns
|
|
113
176
|
|
|
114
177
|
### Production Mode Data Fetching
|