nuxt-link-checker 5.0.9 → 5.1.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/dist/devtools/components/link-checker/CodeDiff.vue +55 -0
- package/dist/devtools/components/link-checker/CodeHighlight.vue +35 -0
- package/dist/devtools/components/link-checker/FixActionDialog.vue +58 -0
- package/dist/devtools/components/link-checker/LinkInspection.vue +108 -0
- package/dist/devtools/components/link-checker/LinkPassing.vue +23 -0
- package/dist/devtools/lib/link-checker/dialog.ts +1 -0
- package/dist/devtools/lib/link-checker/rpc-types.ts +1 -0
- package/dist/devtools/lib/link-checker/rpc.ts +45 -0
- package/dist/devtools/lib/link-checker/state.ts +53 -0
- package/dist/devtools/lib/link-checker/types.ts +1 -0
- package/dist/devtools/nuxt.config.ts +7 -0
- package/dist/devtools/pages/link-checker/debug.vue +25 -0
- package/dist/devtools/pages/link-checker/docs.vue +3 -0
- package/dist/devtools/pages/link-checker/index.vue +73 -0
- package/dist/devtools/pages/link-checker/links.vue +16 -0
- package/dist/devtools/pages/link-checker.vue +86 -0
- package/dist/eslint.mjs +10 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +6 -6
- package/dist/runtime/app/plugins/view/client.d.ts +1 -1
- package/dist/runtime/app/plugins/view/client.js +3 -2
- package/dist/runtime/server/providers/content-v3.js +1 -1
- package/dist/runtime/server/routes/__link-checker__/debug.js +1 -1
- package/dist/runtime/server/routes/__link-checker__/inspect.js +1 -1
- package/dist/runtime/server/routes/__link-checker__/links.js +1 -1
- package/package.json +31 -30
- package/dist/devtools/200.html +0 -1
- package/dist/devtools/404.html +0 -1
- package/dist/devtools/_nuxt/B8PEiB0p.js +0 -1
- package/dist/devtools/_nuxt/BGPtaqnr.js +0 -1
- package/dist/devtools/_nuxt/CVO1_9PV.js +0 -1
- package/dist/devtools/_nuxt/Cp-IABpG.js +0 -1
- package/dist/devtools/_nuxt/D0r3Knsf.js +0 -1
- package/dist/devtools/_nuxt/builds/latest.json +0 -1
- package/dist/devtools/_nuxt/builds/meta/eecfe483-dacc-4e85-91af-c4cc23bfceaa.json +0 -1
- package/dist/devtools/_nuxt/entry.BHWSa69F.css +0 -1
- package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
- package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
- package/dist/devtools/_nuxt/wDzz0qaB.js +0 -1
- package/dist/devtools/_nuxt/y7unhDtB.js +0 -183
- package/dist/devtools/index.html +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BundledLanguage } from 'shiki'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
code: string
|
|
6
|
+
lang?: BundledLanguage
|
|
7
|
+
diff: { added: number[], removed: number[], result: string }
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const start = ref(props.diff.added[0] - 2)
|
|
11
|
+
|
|
12
|
+
const shikiClassRe = /class="shiki/
|
|
13
|
+
const lineClassRe = /class="line"/g
|
|
14
|
+
const codeContentRe = /<code>([\s\S]*)<\/code>/
|
|
15
|
+
|
|
16
|
+
function transformRendered(code: string) {
|
|
17
|
+
let count = 0
|
|
18
|
+
const linesToInclude = new Set<number>()
|
|
19
|
+
const diffed = code
|
|
20
|
+
.replace(shikiClassRe, 'class="shiki diff')
|
|
21
|
+
.replace(lineClassRe, (_) => {
|
|
22
|
+
count++
|
|
23
|
+
const hasAdded = props.diff.added.includes(count - 1)
|
|
24
|
+
const hasRemoved = props.diff.removed.includes(count - 1)
|
|
25
|
+
if (hasAdded || hasRemoved) {
|
|
26
|
+
for (let i = count - 3; i < count + 3; i++) {
|
|
27
|
+
if (i >= 0)
|
|
28
|
+
linesToInclude.add(i)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (hasAdded)
|
|
32
|
+
return 'class="line line-added"'
|
|
33
|
+
if (hasRemoved)
|
|
34
|
+
return 'class="line line-removed"'
|
|
35
|
+
return _
|
|
36
|
+
})
|
|
37
|
+
return diffed.replace(codeContentRe, (_, p1) => {
|
|
38
|
+
const lines = p1.split('\n')
|
|
39
|
+
const filtered = lines.filter((_: any, i: number) => linesToInclude.has(i + 1))
|
|
40
|
+
return `<code>${filtered.join('\n')}</code>`
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const elRef = useTemplateRef<HTMLDivElement>('elRef')
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<OCodeBlock
|
|
49
|
+
ref="elRef"
|
|
50
|
+
:code="diff.result"
|
|
51
|
+
:lang="lang"
|
|
52
|
+
:transform-rendered="transformRendered"
|
|
53
|
+
:style="`--start: ${start};`"
|
|
54
|
+
/>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BundledLanguage } from 'shiki'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
code: string
|
|
6
|
+
lang: BundledLanguage
|
|
7
|
+
link: string
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
function transformRendered(code: string) {
|
|
11
|
+
return code.replaceAll(props.link, `<span class="highlight">${props.link}</span>`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const elRef = useTemplateRef<HTMLDivElement>('elRef')
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<OCodeBlock
|
|
19
|
+
ref="elRef"
|
|
20
|
+
:code="code"
|
|
21
|
+
:lang="lang"
|
|
22
|
+
:transform-rendered="transformRendered"
|
|
23
|
+
v-bind="$attrs"
|
|
24
|
+
/>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
.highlight {
|
|
29
|
+
background-color: #ffd5d5;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.dark .highlight {
|
|
33
|
+
background-color: #5c0000;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { FixDialog } from '../../lib/link-checker/dialog'
|
|
3
|
+
import { host } from '../../lib/link-checker/rpc'
|
|
4
|
+
|
|
5
|
+
function openFilePath(filepath: string) {
|
|
6
|
+
host.value?.openInEditor(filepath)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function handleClose(_a: any, resolve: (value: boolean) => void) {
|
|
10
|
+
resolve(false)
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<FixDialog v-slot="{ resolve, args }">
|
|
16
|
+
<UModal :open="true" @close="handleClose('close', resolve)">
|
|
17
|
+
<div class="flex flex-col gap-2 w-full p-6">
|
|
18
|
+
<h2 class="text-xl font-semibold text-[var(--color-primary)]">
|
|
19
|
+
Confirm Code Diff
|
|
20
|
+
</h2>
|
|
21
|
+
|
|
22
|
+
<div v-for="(source, i) in args[0].diff" :key="i">
|
|
23
|
+
<div class="flex items-center gap-2 mb-1">
|
|
24
|
+
<div class="text-sm gap-2 flex flex-row mb-1 justify-end">
|
|
25
|
+
<span class="text-green-500">+{{ source.diff.added.length }}</span>
|
|
26
|
+
<span class="text-red-500">-{{ source.diff.removed.length }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="opacity-50 text-xs font-mono cursor-pointer hover:opacity-80"
|
|
31
|
+
@click="openFilePath(source.filepath)"
|
|
32
|
+
>
|
|
33
|
+
{{ source.filepath }}
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="rounded-lg border border-[var(--color-border)] overflow-hidden max-h-50 overflow-auto">
|
|
38
|
+
<div class="flex flex-col gap-1 items-start px-4 py-2">
|
|
39
|
+
<CodeDiff v-bind="source" lang="vue-html" class="overflow-auto" />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="flex gap-3 mt-2 justify-end items-center">
|
|
45
|
+
<DevtoolsAlert variant="info" class="flex-auto text-sm">
|
|
46
|
+
Experimental.
|
|
47
|
+
</DevtoolsAlert>
|
|
48
|
+
<UButton variant="ghost" @click="resolve(false)">
|
|
49
|
+
Cancel
|
|
50
|
+
</UButton>
|
|
51
|
+
<UButton color="primary" @click="resolve(true)">
|
|
52
|
+
Apply Fix
|
|
53
|
+
</UButton>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</UModal>
|
|
57
|
+
</FixDialog>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { FixDialog } from '../../lib/link-checker/dialog'
|
|
3
|
+
import { host, linkCheckerRpc } from '../../lib/link-checker/rpc'
|
|
4
|
+
|
|
5
|
+
const { item } = defineProps<{
|
|
6
|
+
item: any
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
function openFilePath(filepath: string) {
|
|
10
|
+
host.value?.openInEditor(filepath)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function fixDialog() {
|
|
14
|
+
if (!await FixDialog.start(item))
|
|
15
|
+
return
|
|
16
|
+
await linkCheckerRpc.value!.applyLinkFixes(item.diff, item.link, item.fix)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function scrollToLink() {
|
|
20
|
+
await linkCheckerRpc.value!.scrollToLink(item.link)
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div v-if="item" class="text-sm lg:flex w-full gap-5">
|
|
26
|
+
<div class="flex-shrink-0">
|
|
27
|
+
<UButton variant="ghost" size="xs" icon="carbon:cursor-2" @click="scrollToLink" />
|
|
28
|
+
</div>
|
|
29
|
+
<div class="min-w-0 flex-1">
|
|
30
|
+
<div class="flex space-x-2">
|
|
31
|
+
<UTooltip :text="item.textContent">
|
|
32
|
+
<div class="opacity-90 truncate" style="max-width: 150px;">
|
|
33
|
+
{{ item.textContent }}
|
|
34
|
+
</div>
|
|
35
|
+
</UTooltip>
|
|
36
|
+
<a :href="item.link" target="_blank" class="font-mono mb-3 truncate text-[var(--color-primary)] hover:underline">
|
|
37
|
+
{{ item.link }}
|
|
38
|
+
</a>
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
<div v-for="(inspection, i) in [...item.error, ...item.warning]" :key="i" class="flex flex-row gap-2 items-center mb-2">
|
|
42
|
+
<div class="flex gap-2 w-full">
|
|
43
|
+
<div style="min-width: 150px; margin-top: 1px;">
|
|
44
|
+
<template v-if="inspection.scope === 'error'">
|
|
45
|
+
<div class="text-red-800 text-xs flex items-center font-medium dark:text-red-300">
|
|
46
|
+
<UIcon name="carbon:error" class="mr-1 text-xs" />
|
|
47
|
+
Error
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
<template v-else>
|
|
51
|
+
<div class="text-yellow-800 flex items-center text-xs font-medium dark:text-yellow-300">
|
|
52
|
+
<UIcon name="carbon:warning" class="mr-1 text-xs" />
|
|
53
|
+
Warning
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
<span class="text-[var(--color-text-muted)]">{{ inspection.name }}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="flex-grow">
|
|
59
|
+
<div>{{ inspection.message }}</div>
|
|
60
|
+
<div v-if="inspection.tip">
|
|
61
|
+
<span class="opacity-60">{{ inspection.tip }}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<UButton
|
|
64
|
+
v-if="inspection.fix"
|
|
65
|
+
variant="outline"
|
|
66
|
+
color="green"
|
|
67
|
+
size="xs"
|
|
68
|
+
icon="carbon:magic-wand"
|
|
69
|
+
:label="inspection.fixDescription"
|
|
70
|
+
class="mt-2"
|
|
71
|
+
@click="fixDialog"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<template v-if="item.passes">
|
|
79
|
+
<div class="flex gap-2 items-center">
|
|
80
|
+
<div style="flex-basis: 4rem;">
|
|
81
|
+
<span class="text-green-800 text-xs flex items-center font-medium dark:text-green-300">
|
|
82
|
+
<UIcon name="carbon:checkmark-outline" class="mr-1 text-xs" />
|
|
83
|
+
Pass
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
<div v-else class="flex flex-col w-full">
|
|
89
|
+
<div v-if="!item.sources?.length" class="text-[var(--color-text-muted)]">
|
|
90
|
+
No source code found.
|
|
91
|
+
</div>
|
|
92
|
+
<div v-for="(source, i) in item.sources" :key="i" class="mb-4">
|
|
93
|
+
<div class="flex mb-1 items-center">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
class="opacity-50 text-xs font-mono cursor-pointer hover:opacity-80"
|
|
97
|
+
@click="openFilePath(source.filepath)"
|
|
98
|
+
>
|
|
99
|
+
{{ source.filepath }}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
<div v-for="(preview, pk) in source.previews" :key="pk" class="rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
103
|
+
<CodeHighlight :link="item.link" :code="preview.code" lang="vue-html" class="overflow-auto" :style="{ '--start': Math.max(preview.lineNumber - 2, 1) }" />
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
item: any
|
|
4
|
+
}>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="text-sm flex w-full gap-5">
|
|
9
|
+
<div class="min-w-0 flex-1">
|
|
10
|
+
<a :href="item.link" target="_blank" class="font-mono mb-3 text-[var(--color-primary)] hover:underline">
|
|
11
|
+
{{ item.link }}
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="flex gap-2 items-center">
|
|
15
|
+
<div style="flex-basis: 4rem;">
|
|
16
|
+
<span class="text-green-800 text-xs flex items-center font-medium dark:text-green-300">
|
|
17
|
+
<UIcon name="carbon:checkmark-outline" class="mr-1 text-xs" />
|
|
18
|
+
Pass
|
|
19
|
+
</span>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const FixDialog = createTemplatePromise<boolean, [any]>()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { ClientFunctions, ServerFunctions } from '../../../src/rpc-types'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BirpcReturn } from 'birpc'
|
|
2
|
+
import type { ClientFunctions, ServerFunctions } from './rpc-types'
|
|
3
|
+
import type { NuxtLinkCheckerClient } from './types'
|
|
4
|
+
import { useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc'
|
|
5
|
+
import { getQuery } from 'ufo'
|
|
6
|
+
import { ref, unref } from 'vue'
|
|
7
|
+
import { linkDb, linkFilter, queueLength, visibleLinks } from './state'
|
|
8
|
+
|
|
9
|
+
const RPC_NAMESPACE = 'nuxt-link-checker-rpc'
|
|
10
|
+
|
|
11
|
+
export const host = ref<any>()
|
|
12
|
+
export const linkCheckerRpc = ref<BirpcReturn<ServerFunctions>>()
|
|
13
|
+
|
|
14
|
+
useDevtoolsConnection({
|
|
15
|
+
onConnected(connectedHost) {
|
|
16
|
+
host.value = connectedHost
|
|
17
|
+
|
|
18
|
+
const linkCheckerClient = connectedHost.inject<NuxtLinkCheckerClient>('linkChecker')
|
|
19
|
+
// The injected link-checker client holds the live link data; without it there
|
|
20
|
+
// is nothing to mirror, so finish with the layer's data refresh (already fired).
|
|
21
|
+
if (!linkCheckerClient)
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
linkDb.value = linkCheckerClient.linkDb.value
|
|
25
|
+
visibleLinks.value = [...linkCheckerClient.visibleLinks]
|
|
26
|
+
linkFilter.value = getQuery(window.location.href).link as string
|
|
27
|
+
|
|
28
|
+
const rpc = connectedHost.rpc<ServerFunctions, ClientFunctions>(RPC_NAMESPACE, {
|
|
29
|
+
queueWorking(payload) {
|
|
30
|
+
queueLength.value = payload.queueLength
|
|
31
|
+
linkDb.value = unref(linkCheckerClient.linkDb)
|
|
32
|
+
visibleLinks.value = [...linkCheckerClient.visibleLinks]
|
|
33
|
+
},
|
|
34
|
+
updated() {
|
|
35
|
+
linkDb.value = unref(linkCheckerClient.linkDb)
|
|
36
|
+
visibleLinks.value = [...linkCheckerClient.visibleLinks]
|
|
37
|
+
},
|
|
38
|
+
filter(payload) {
|
|
39
|
+
linkFilter.value = payload.link
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
linkCheckerRpc.value = rpc
|
|
43
|
+
rpc?.connected()
|
|
44
|
+
},
|
|
45
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LinkInspectionResult } from './types'
|
|
2
|
+
import { useLocalStorage } from '@vueuse/core'
|
|
3
|
+
import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'
|
|
4
|
+
import { refreshTime } from 'nuxtseo-layer-devtools/composables/state'
|
|
5
|
+
import { computed, ref } from 'vue'
|
|
6
|
+
import { useAsyncData } from '#imports'
|
|
7
|
+
|
|
8
|
+
export const linkDb = ref<LinkInspectionResult[]>([])
|
|
9
|
+
export const showLiveInspections = useLocalStorage<boolean>('nuxt-link-checker:show-live-inspections', true)
|
|
10
|
+
export const visibleLinks = ref<string[]>([])
|
|
11
|
+
export const queueLength = ref(0)
|
|
12
|
+
|
|
13
|
+
export const linkFilter = ref<string | false>('')
|
|
14
|
+
|
|
15
|
+
// Derived link views shared across the Inspections and Links tabs
|
|
16
|
+
export const nodes = computed(() => {
|
|
17
|
+
const validLinks = visibleLinks.value
|
|
18
|
+
let n = [...linkDb.value]
|
|
19
|
+
if (linkFilter.value)
|
|
20
|
+
n = n.filter(node => node.link === linkFilter.value)
|
|
21
|
+
else
|
|
22
|
+
n = n.filter(node => validLinks.includes(node.link))
|
|
23
|
+
return n.sort((a, b) => (a.fix && !b.fix ? 1 : -1))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const failingNodes = computed(() => {
|
|
27
|
+
const seen = new Set<string>()
|
|
28
|
+
return nodes.value.filter((n) => {
|
|
29
|
+
if (!n.error.length && !n.warning.length)
|
|
30
|
+
return false
|
|
31
|
+
if (seen.has(n.link))
|
|
32
|
+
return false
|
|
33
|
+
seen.add(n.link)
|
|
34
|
+
return true
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const internalLinks = computed(() => nodes.value.filter(n => n.passes && n.link.startsWith('/')))
|
|
39
|
+
export const externalLinks = computed(() => nodes.value.filter(n => n.passes && !n.link.startsWith('/')))
|
|
40
|
+
export const errorCount = computed(() => nodes.value.reduce((count, n) => count + n.error.length, 0))
|
|
41
|
+
export const warningCount = computed(() => nodes.value.reduce((count, n) => count + n.warning.length, 0))
|
|
42
|
+
export const visibleLinkCount = computed(() => visibleLinks.value.length)
|
|
43
|
+
|
|
44
|
+
export function useDebugData(): any {
|
|
45
|
+
return useAsyncData<{ runtimeConfig: any } | null>('link-checker-debug', () => {
|
|
46
|
+
if (!appFetch.value)
|
|
47
|
+
return null
|
|
48
|
+
return appFetch.value('/__link-checker__/debug.json')
|
|
49
|
+
}, {
|
|
50
|
+
watch: [appFetch, refreshTime],
|
|
51
|
+
default: () => null,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { LinkInspectionResult, NuxtLinkCheckerClient } from '../../../src/runtime/types'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolve } from 'pathe'
|
|
2
|
+
|
|
3
|
+
// Nuxt SEO devtools panel, shipped as a layer (Model C). Components flat-registered
|
|
4
|
+
// so intra-panel references resolve by name.
|
|
5
|
+
export default defineNuxtConfig({
|
|
6
|
+
components: [{ path: resolve(__dirname, './components'), pathPrefix: false }],
|
|
7
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useDebugData } from '../../lib/link-checker/state'
|
|
4
|
+
|
|
5
|
+
const { data } = useDebugData()
|
|
6
|
+
|
|
7
|
+
const runtimeConfigItems = computed(() => {
|
|
8
|
+
const config = data.value?.runtimeConfig || {}
|
|
9
|
+
return Object.entries(config)
|
|
10
|
+
.filter(([key]) => key !== 'version')
|
|
11
|
+
.map(([key, value]) => ({
|
|
12
|
+
key,
|
|
13
|
+
value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value ?? ''),
|
|
14
|
+
mono: true,
|
|
15
|
+
copyable: true,
|
|
16
|
+
code: typeof value === 'object' ? 'json' as const : undefined,
|
|
17
|
+
}))
|
|
18
|
+
})
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<DevtoolsSection icon="carbon:settings" text="Runtime Config">
|
|
23
|
+
<DevtoolsKeyValue :items="runtimeConfigItems" striped />
|
|
24
|
+
</DevtoolsSection>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import FixActionDialog from '../../components/link-checker/FixActionDialog.vue'
|
|
3
|
+
import {
|
|
4
|
+
errorCount,
|
|
5
|
+
failingNodes,
|
|
6
|
+
linkFilter,
|
|
7
|
+
nodes,
|
|
8
|
+
queueLength,
|
|
9
|
+
visibleLinkCount,
|
|
10
|
+
warningCount,
|
|
11
|
+
} from '../../lib/link-checker/state'
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="space-y-2">
|
|
16
|
+
<DevtoolsToolbar>
|
|
17
|
+
<DevtoolsMetric
|
|
18
|
+
v-if="queueLength"
|
|
19
|
+
icon="carbon:progress-bar-round"
|
|
20
|
+
:value="`${visibleLinkCount ? Math.round((Math.abs(queueLength - visibleLinkCount) / visibleLinkCount) * 100) : 0}%`"
|
|
21
|
+
/>
|
|
22
|
+
<DevtoolsMetric
|
|
23
|
+
v-if="errorCount"
|
|
24
|
+
icon="carbon:error"
|
|
25
|
+
:value="errorCount"
|
|
26
|
+
label="Errors"
|
|
27
|
+
variant="danger"
|
|
28
|
+
/>
|
|
29
|
+
<DevtoolsMetric
|
|
30
|
+
v-if="warningCount"
|
|
31
|
+
icon="carbon:warning"
|
|
32
|
+
:value="warningCount"
|
|
33
|
+
label="Warnings"
|
|
34
|
+
variant="warning"
|
|
35
|
+
/>
|
|
36
|
+
<DevtoolsMetric
|
|
37
|
+
v-if="!warningCount && !errorCount"
|
|
38
|
+
icon="carbon:checkmark-outline"
|
|
39
|
+
value="All links passing"
|
|
40
|
+
variant="success"
|
|
41
|
+
/>
|
|
42
|
+
</DevtoolsToolbar>
|
|
43
|
+
|
|
44
|
+
<p v-if="linkFilter" class="text-sm flex items-center gap-1 mb-4">
|
|
45
|
+
<UIcon name="carbon:filter" />
|
|
46
|
+
Filtering Results.
|
|
47
|
+
<button type="button" class="underline" @click="linkFilter = false">
|
|
48
|
+
Show All
|
|
49
|
+
</button>
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<div v-if="!linkFilter">
|
|
53
|
+
<template v-if="failingNodes.length">
|
|
54
|
+
<LinkInspection
|
|
55
|
+
v-for="(item, index) of failingNodes"
|
|
56
|
+
:key="index"
|
|
57
|
+
:item="item"
|
|
58
|
+
class="odd:bg-[var(--color-bg-elevated)] p-2"
|
|
59
|
+
/>
|
|
60
|
+
</template>
|
|
61
|
+
<DevtoolsEmptyState
|
|
62
|
+
v-else-if="!queueLength"
|
|
63
|
+
icon="carbon:checkmark-outline"
|
|
64
|
+
title="No issues found"
|
|
65
|
+
description="All visible links are passing validation."
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div v-else>
|
|
69
|
+
<LinkInspection v-for="(item, index) of nodes" :key="index" :item="item" />
|
|
70
|
+
</div>
|
|
71
|
+
<FixActionDialog />
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { externalLinks, internalLinks } from '../../lib/link-checker/state'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<div class="space-y-4">
|
|
7
|
+
<DevtoolsSection icon="carbon:chart-network" text="Internal Links">
|
|
8
|
+
<LinkPassing v-for="(item, index) of internalLinks" :key="index" :item="item" />
|
|
9
|
+
<DevtoolsEmptyState v-if="!internalLinks.length" icon="carbon:link" title="No internal links" />
|
|
10
|
+
</DevtoolsSection>
|
|
11
|
+
<DevtoolsSection icon="carbon:launch" text="External Links">
|
|
12
|
+
<LinkPassing v-for="(item, index) of externalLinks" :key="index" :item="item" />
|
|
13
|
+
<DevtoolsEmptyState v-if="!externalLinks.length" icon="carbon:link" title="No external links" />
|
|
14
|
+
</DevtoolsSection>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { loadShiki } from 'nuxtseo-layer-devtools/composables/shiki'
|
|
3
|
+
import { isProductionMode, refreshSources } from 'nuxtseo-layer-devtools/composables/state'
|
|
4
|
+
import { computed, ref, watch } from 'vue'
|
|
5
|
+
import { navigateTo, useRoute } from '#imports'
|
|
6
|
+
import { linkCheckerRpc } from '../lib/link-checker/rpc'
|
|
7
|
+
import { linkDb, queueLength, showLiveInspections, useDebugData } from '../lib/link-checker/state'
|
|
8
|
+
|
|
9
|
+
await loadShiki({
|
|
10
|
+
extraLangs: [
|
|
11
|
+
import('@shikijs/langs/vue-html'),
|
|
12
|
+
],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const { data } = await useDebugData()
|
|
16
|
+
|
|
17
|
+
const route = useRoute()
|
|
18
|
+
const currentTab = computed(() => {
|
|
19
|
+
const p = route.path
|
|
20
|
+
if (p.startsWith('/link-checker/links'))
|
|
21
|
+
return 'links'
|
|
22
|
+
if (p.startsWith('/link-checker/debug'))
|
|
23
|
+
return 'debug'
|
|
24
|
+
if (p.startsWith('/link-checker/docs'))
|
|
25
|
+
return 'docs'
|
|
26
|
+
return 'inspections'
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const navItems = [
|
|
30
|
+
{ value: 'inspections', to: '/link-checker', icon: 'carbon:warning-diamond', label: 'Inspections', devOnly: false },
|
|
31
|
+
{ value: 'links', to: '/link-checker/links', icon: 'carbon:checkmark-outline', label: 'Links', devOnly: false },
|
|
32
|
+
{ value: 'debug', to: '/link-checker/debug', icon: 'carbon:debug', label: 'Debug', devOnly: true },
|
|
33
|
+
{ value: 'docs', to: '/link-checker/docs', icon: 'carbon:book', label: 'Docs', devOnly: false },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const loading = ref(false)
|
|
37
|
+
|
|
38
|
+
async function retryAll() {
|
|
39
|
+
linkDb.value = []
|
|
40
|
+
queueLength.value = 0
|
|
41
|
+
await linkCheckerRpc.value!.reset()
|
|
42
|
+
}
|
|
43
|
+
async function toggleLiveInspections() {
|
|
44
|
+
showLiveInspections.value = !showLiveInspections.value
|
|
45
|
+
await linkCheckerRpc.value!.toggleLiveInspections(showLiveInspections.value)
|
|
46
|
+
}
|
|
47
|
+
async function refresh() {
|
|
48
|
+
loading.value = true
|
|
49
|
+
await retryAll()
|
|
50
|
+
refreshSources()
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
loading.value = false
|
|
53
|
+
}, 300)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Debug data is dev-only; leave the debug tab when the header switches to Production
|
|
57
|
+
watch(isProductionMode, (isProd) => {
|
|
58
|
+
if (isProd && currentTab.value === 'debug')
|
|
59
|
+
return navigateTo('/link-checker')
|
|
60
|
+
})
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<DevtoolsLayout
|
|
65
|
+
v-model:active-tab="currentTab"
|
|
66
|
+
module-name="nuxt-link-checker"
|
|
67
|
+
title="Link Checker"
|
|
68
|
+
icon="carbon:cloud-satellite-link"
|
|
69
|
+
:version="data?.runtimeConfig?.version"
|
|
70
|
+
:nav-items="navItems"
|
|
71
|
+
github-url="https://github.com/harlan-zw/nuxt-link-checker"
|
|
72
|
+
:loading="loading"
|
|
73
|
+
@refresh="refresh"
|
|
74
|
+
>
|
|
75
|
+
<template #actions>
|
|
76
|
+
<UButton
|
|
77
|
+
:icon="showLiveInspections ? 'carbon:view-off' : 'carbon:view'"
|
|
78
|
+
variant="ghost"
|
|
79
|
+
size="xs"
|
|
80
|
+
:label="showLiveInspections ? 'Hide Inspections' : 'Show Inspections'"
|
|
81
|
+
@click="toggleLiveInspections"
|
|
82
|
+
/>
|
|
83
|
+
</template>
|
|
84
|
+
<NuxtPage />
|
|
85
|
+
</DevtoolsLayout>
|
|
86
|
+
</template>
|
package/dist/eslint.mjs
CHANGED
|
@@ -28,6 +28,13 @@ function extractMarkdownLinks(text) {
|
|
|
28
28
|
return links;
|
|
29
29
|
}
|
|
30
30
|
const lineMapStack = [];
|
|
31
|
+
const OWN_RULES = /* @__PURE__ */ new Set(["valid-route", "valid-sitemap-link"]);
|
|
32
|
+
function isOwnRule(ruleId) {
|
|
33
|
+
if (!ruleId)
|
|
34
|
+
return false;
|
|
35
|
+
const name = ruleId.includes("/") ? ruleId.slice(ruleId.lastIndexOf("/") + 1) : ruleId;
|
|
36
|
+
return OWN_RULES.has(name);
|
|
37
|
+
}
|
|
31
38
|
const markdownProcessor = {
|
|
32
39
|
meta: {
|
|
33
40
|
name: "link-checker/markdown",
|
|
@@ -54,9 +61,10 @@ const markdownProcessor = {
|
|
|
54
61
|
},
|
|
55
62
|
postprocess(messages) {
|
|
56
63
|
const lineMap = lineMapStack.pop();
|
|
64
|
+
const filtered = messages.flat().filter((msg) => isOwnRule(msg.ruleId));
|
|
57
65
|
if (!lineMap || !lineMap.size)
|
|
58
|
-
return
|
|
59
|
-
return
|
|
66
|
+
return filtered;
|
|
67
|
+
return filtered.map((msg) => {
|
|
60
68
|
const link = lineMap.get(msg.line);
|
|
61
69
|
if (link) {
|
|
62
70
|
return {
|