Skip to content

Commit 9aa000d

Browse files
committed
feat(devtools): improved tracking
1 parent 6571544 commit 9aa000d

File tree

7 files changed

+582
-108
lines changed

7 files changed

+582
-108
lines changed

client/app.vue

Lines changed: 403 additions & 97 deletions
Large diffs are not rendered by default.

client/components/OCodeBlock.vue

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import type { BundledLanguage } from 'shiki'
3+
import { computed } from 'vue'
4+
import { renderCodeHighlight } from '../composables/shiki'
5+
6+
const props = withDefaults(
7+
defineProps<{
8+
code: string
9+
lang?: BundledLanguage
10+
lines?: boolean
11+
transformRendered?: (code: string) => string
12+
}>(),
13+
{
14+
lines: false,
15+
},
16+
)
17+
const rendered = computed(() => {
18+
const code = renderCodeHighlight(props.code, props.lang || 'json')
19+
return props.transformRendered ? props.transformRendered(code.value || '') : code.value
20+
})
21+
</script>
22+
23+
<template>
24+
<pre
25+
class="n-code-block"
26+
:class="lines ? 'n-code-block-lines' : ''"
27+
v-html="rendered"
28+
/>
29+
</template>
30+
31+
<style>
32+
.n-code-block-lines .shiki code .line::before {
33+
display: none;
34+
}
35+
</style>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<div>
3+
<div class="inline-flex items-center px-2 py-0.5 text-xs font-medium m bg-neutral-700 text-green-300 rounded-full">
4+
{{ loadTime }}
5+
</div>
6+
</div>
7+
</template>
8+
9+
<script setup lang="ts">
10+
interface Props {
11+
loadTime?: string
12+
}
13+
14+
defineProps<Props>()
15+
</script>

client/components/ScriptSize.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<div v-if="size" title="kb Size">
3+
<div class="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-neutral-700 text-neutral-300 rounded-full">
4+
{{ size }}
5+
</div>
6+
</div>
7+
</template>
8+
9+
<script setup lang="ts">
10+
interface Props {
11+
size?: string
12+
}
13+
14+
defineProps<Props>()
15+
</script>

client/components/ScriptStatus.vue

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<template>
2+
<div class="flex items-center gap-2">
3+
<div
4+
class="w-2 h-2 rounded-full"
5+
:class="statusColor"
6+
/>
7+
<span class="capitalize text-sm">{{ displayStatus }}</span>
8+
</div>
9+
</template>
10+
11+
<script setup lang="ts">
12+
interface Props {
13+
status?: string
14+
error?: string
15+
}
16+
17+
const props = defineProps<Props>()
18+
19+
const displayStatus = computed(() => {
20+
if (props.error) {
21+
return props.error === 'TypeError: Failed to fetch' ? 'CORS Error' : props.error
22+
}
23+
return props.status || 'unknown'
24+
})
25+
26+
const statusColor = computed(() => {
27+
if (props.error) {
28+
return 'bg-red-500'
29+
}
30+
31+
switch (props.status) {
32+
case 'loaded':
33+
return 'bg-green-500'
34+
case 'loading':
35+
return 'bg-blue-500'
36+
case 'awaitingLoad':
37+
return 'bg-yellow-500'
38+
case 'error':
39+
return 'bg-red-500'
40+
default:
41+
return 'bg-gray-400'
42+
}
43+
})
44+
</script>

client/composables/shiki.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { HighlighterCore } from 'shiki'
2+
import { createHighlighterCore } from 'shiki/core'
3+
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
4+
import { computed, ref, toValue } from 'vue'
5+
import type { MaybeRef } from '@vueuse/core'
6+
import { devtools } from './rpc'
7+
8+
export const shiki = ref<HighlighterCore>()
9+
10+
export function loadShiki() {
11+
// Only loading when needed
12+
return createHighlighterCore({
13+
themes: [
14+
import('@shikijs/themes/vitesse-light'),
15+
import('@shikijs/themes/vitesse-dark'),
16+
],
17+
langs: [
18+
import('@shikijs/langs/json'),
19+
import('@shikijs/langs/typescript'),
20+
],
21+
engine: createJavaScriptRegexEngine(),
22+
}).then((i) => {
23+
shiki.value = i
24+
})
25+
}
26+
27+
export function renderCodeHighlight(code: MaybeRef<string>, lang: 'json' | 'typescript') {
28+
return computed(() => {
29+
const colorMode = devtools.value?.colorMode || 'light'
30+
return shiki.value!.codeToHtml(toValue(code), {
31+
lang,
32+
theme: colorMode === 'dark' ? 'vitesse-dark' : 'vitesse-light',
33+
}) || ''
34+
})
35+
}

src/runtime/utils.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,41 @@ export function useRegistryScript<T extends Record<string | symbol, any>, O = Em
7474
const scriptInput = defu(finalScriptInput, userOptions.scriptInput, { key: registryKey }) as any as UseScriptInput
7575
const scriptOptions = Object.assign(userOptions?.scriptOptions || {}, options.scriptOptions || {})
7676
if (import.meta.dev) {
77-
scriptOptions.devtools = defu(scriptOptions.devtools, { registryKey })
77+
// Capture where the component was loaded from
78+
const error = new Error('Stack trace for component location')
79+
const stack = error.stack?.split('\n')
80+
const callerLine = stack?.find(line =>
81+
line.includes('.vue')
82+
&& !line.includes('useRegistryScript')
83+
&& !line.includes('node_modules'),
84+
)
85+
86+
let loadedFrom = 'unknown'
87+
if (callerLine) {
88+
// Extract URL pattern like "https://localhost:3000/_nuxt/pages/features/custom-registry.vue?t=1758609859248:14:31"
89+
// Handle both with and without query parameters
90+
const urlMatch = callerLine.match(/https?:\/\/[^/]+\/_nuxt\/(.+\.vue)(?:\?[^)]*)?:(\d+):(\d+)/)
91+
|| callerLine.match(/\(https?:\/\/[^/]+\/_nuxt\/(.+\.vue)(?:\?[^)]*)?:(\d+):(\d+)\)/)
92+
93+
if (urlMatch) {
94+
const [, filePath, line, column] = urlMatch
95+
loadedFrom = `./${filePath}:${line}:${column}`
96+
}
97+
else {
98+
// Try to extract any .vue file path with line:column
99+
const vueMatch = callerLine.match(/([^/\s]+\.vue):(\d+):(\d+)/)
100+
if (vueMatch) {
101+
const [, fileName, line, column] = vueMatch
102+
loadedFrom = `./${fileName}:${line}:${column}`
103+
}
104+
else {
105+
// Fallback to original cleaning
106+
loadedFrom = callerLine.trim().replace(/^\s*at\s+/, '')
107+
}
108+
}
109+
}
110+
111+
scriptOptions.devtools = defu(scriptOptions.devtools, { registryKey, loadedFrom })
78112
if (options.schema) {
79113
const registryMeta: Record<string, string> = {}
80114
for (const k in options.schema.entries) {
@@ -107,13 +141,3 @@ export function useRegistryScript<T extends Record<string | symbol, any>, O = Em
107141
}
108142
return useScript<T>(scriptInput, scriptOptions as NuxtUseScriptOptions<T>)
109143
}
110-
111-
export function pick(obj: Record<string, any>, keys: string[]) {
112-
const res: Record<string, any> = {}
113-
for (const k of keys) {
114-
if (k in obj) {
115-
res[k] = obj[k]
116-
}
117-
}
118-
return res
119-
}

0 commit comments

Comments
 (0)