Skip to content

Commit 8794d74

Browse files
committed
fix(devtools): support multiple of the same registry scripts
1 parent 2eedd4a commit 8794d74

File tree

7 files changed

+131
-46
lines changed

7 files changed

+131
-46
lines changed

Diff for: client/app.vue

+79-32
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,42 @@ import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
33
import { registry } from '../src/registry'
44
import { devtools, getScriptSize, humanFriendlyTimestamp, reactive, ref, urlToOrigin } from '#imports'
55
import { loadShiki } from '~/composables/shiki'
6+
import { msToHumanReadable } from '~/utils/formatting'
67
78
const scriptRegistry = registry(s => s)
89
await loadShiki()
910
1011
const scripts = ref({})
11-
const scriptSizes = reactive({})
12+
const scriptSizes = reactive<Record<string, string>>({})
1213
1314
function syncScripts(_scripts: any[]) {
14-
scripts.value = { ..._scripts }
15-
// check if the script size has been set, if not set it
16-
for (const key in _scripts) {
17-
if (!scriptSizes[key]) {
18-
getScriptSize(_scripts[key].src).then((size) => {
19-
scriptSizes[key] = size
20-
}).catch(() => {
21-
scriptSizes[key] = 0
22-
})
23-
}
24-
}
15+
// augment the scripts with registry
16+
scripts.value = Object.fromEntries(
17+
Object.entries({ ..._scripts })
18+
.map(([key, script]) => {
19+
script.registry = scriptRegistry.find(s => titleToCamelCase(s.label) === script.registryKey)
20+
if (script.registry) {
21+
const kebabCaseLabel = script.registry.label.toLowerCase().replace(/ /g, '-')
22+
script.docs = `https://scripts.nuxt.com/scripts/${script.registry.category}/${kebabCaseLabel}`
23+
}
24+
const loadingAt = script.events.find(e => e.status === 'loading')?.at || 0
25+
const loadedAt = script.events.find(e => e.status === 'loaded')?.at || 0
26+
if (loadingAt && loadedAt) {
27+
script.loadTime = msToHumanReadable(loadedAt - loadingAt)
28+
}
29+
const scriptSizeKey = script.src
30+
if (!scriptSizes[scriptSizeKey]) {
31+
getScriptSize(script.src).then((size) => {
32+
scriptSizes[scriptSizeKey] = size
33+
script.size = size
34+
}).catch(() => {
35+
script.size = ''
36+
scriptSizes[scriptSizeKey] = ''
37+
})
38+
}
39+
return [key, script]
40+
}),
41+
)
2542
}
2643
2744
function titleToCamelCase(s: string) {
@@ -31,9 +48,7 @@ function titleToCamelCase(s: string) {
3148
return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
3249
}).join('')
3350
}
34-
function resolveScriptRegistry(id: string) {
35-
return scriptRegistry.find(s => titleToCamelCase(s.label) === id)
36-
}
51+
3752
const version = ref(null)
3853
onDevtoolsClientConnected(async (client) => {
3954
devtools.value = client.devtools
@@ -44,6 +59,16 @@ onDevtoolsClientConnected(async (client) => {
4459
syncScripts(client.host.nuxt._scripts)
4560
})
4661
const tab = ref('scripts')
62+
63+
function viewDocs(docs: string) {
64+
tab.value = 'docs'
65+
setTimeout(() => {
66+
const iframe = document.querySelector('iframe')
67+
if (iframe) {
68+
iframe.src = docs
69+
}
70+
}, 100)
71+
}
4772
</script>
4873

4974
<template>
@@ -116,14 +141,6 @@ const tab = ref('scripts')
116141
>
117142
</label>
118143
</fieldset>
119-
<!-- <VTooltip> -->
120-
<!-- <button text-lg="" type="button" class="n-icon-button n-button n-transition n-disabled:n-disabled"> -->
121-
<!-- <NIcon icon="carbon:reset" class="group-hover:text-green-500" /> -->
122-
<!-- </button> -->
123-
<!-- <template #popper> -->
124-
<!-- Refresh -->
125-
<!-- </template> -->
126-
<!-- </VTooltip> -->
127144
</div>
128145
<div class="items-center space-x-3 hidden lg:flex">
129146
<div class="opacity-80 text-sm">
@@ -147,19 +164,49 @@ const tab = ref('scripts')
147164
<div class="space-y-3">
148165
<OSectionBlock v-for="(script, id) in scripts" :key="id" class="w-full">
149166
<template #text>
150-
<div class="flex items-center justify-between w-full">
151-
<div class="flex items-center gap-4">
152-
<a class="text-lg font-bold flex gap-2 items-center font-mono" :title="script.src" target="_blank" :href="script.src">
153-
<div v-if="resolveScriptRegistry(id)" class="flex items-center max-w-10 h-6" v-html="resolveScriptRegistry(id).logo" />
167+
<div class="flex items-center justify-between w-full gap-7">
168+
<div class="flex items-center gap-7">
169+
<div class="flex items-center gap-1">
170+
<div v-if="script.registry" class="flex items-center max-w-6 h-6" v-html="script.registry.logo" />
154171
<img v-else-if="!script.src.startsWith('/')" :src="`https://www.google.com/s2/favicons?domain=${urlToOrigin(script.src)}`" class="w-4 h-4 rounded-lg">
155-
<div>{{ resolveScriptRegistry(id)?.label || script.key }}</div>
156-
</a>
157-
<div class="opacity-70">
172+
<div>
173+
<a title="View script source" class="text-base hover:bg-gray-800/50 px-2 transition py-1 rounded-xl font-semibold flex gap-2 items-center" target="_blank" :href="script.src">
174+
<div>
175+
{{ script.registry?.label || script.key }}
176+
</div>
177+
</a>
178+
<div class="flex flex-items-center gap-3">
179+
<template v-if="script.docs">
180+
<button type="button" class="ml-2 opacity-50 hover:opacity-70 transition ml-1 text-xs underline" @click="viewDocs(script.docs)">
181+
View docs
182+
</button>
183+
</template>
184+
<div v-for="k in Object.keys(script.registryMeta)" :key="k" class="text-xs text-gray-500">
185+
<span class="capitalize">{{ k }}</span>: {{ script.registryMeta[k] }}
186+
</div>
187+
</div>
188+
</div>
189+
</div>
190+
</div>
191+
<div>
192+
<div class="opacity-70 text-xs">
193+
Status
194+
</div>
195+
<div class="capitalize">
158196
{{ script.$script.status.value }}
159197
</div>
160-
<div v-if="scriptSizes[script.key]">
161-
{{ scriptSizes[script.key] }}
198+
</div>
199+
<div v-if="scriptSizes[script.src]">
200+
<div class="opacity-70 text-xs">
201+
Size
202+
</div>
203+
<div>{{ scriptSizes[script.src] }}</div>
204+
</div>
205+
<div v-if="script.loadTime">
206+
<div class="opacity-70 text-xs">
207+
Time to loaded
162208
</div>
209+
<div>{{ script.loadTime }}</div>
163210
</div>
164211
<div>
165212
<NButton v-if="script.$script.status === 'awaitingLoad'" @click="script.$script.load()">

Diff for: client/components/OSectionBlock.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function onToggle(e: any) {
2828

2929
<template>
3030
<details :open="open" @toggle="onToggle">
31-
<summary class="cursor-pointer select-none n-bg-active hover:bg-active p4 rounded transition-all" :class="collapse ? '' : 'pointer-events-none'">
31+
<summary class="cursor-pointer select-none n-bg-active hover:bg-active px-2 py-2 rounded transition-all" :class="collapse ? '' : 'pointer-events-none'">
3232
<NIconTitle :icon="icon" :text="text" text-xl transition :class="[open ? 'op100' : 'op60', headerClass]">
3333
<div>
3434
<div text-base>

Diff for: client/utils/formatting.ts

+10
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,13 @@ export function urlToOrigin(url: string) {
1515
return new URL(url).origin
1616
return url
1717
}
18+
19+
export function msToHumanReadable(ms: number) {
20+
if (ms < 1000) {
21+
return ms + 'ms'
22+
}
23+
if (ms < 60000) {
24+
return (ms / 1000).toFixed(2) + 's'
25+
}
26+
return (ms / 60000).toFixed(2) + 'm'
27+
}

Diff for: src/runtime/composables/useScript.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { hashCode } from '@unhead/shared'
44
import { defu } from 'defu'
55
import { useScript as _useScript } from '@unhead/vue'
66
import { injectHead, onNuxtReady, useNuxtApp, useRuntimeConfig, reactive } from '#imports'
7-
import type { NuxtAppScript, NuxtUseScriptOptions } from '#nuxt-scripts'
7+
import type { NuxtDevToolsScriptInstance, NuxtUseScriptOptions } from '#nuxt-scripts'
88

99
function useNuxtScriptRuntimeConfig() {
1010
return useRuntimeConfig().public['nuxt-scripts'] as {
@@ -41,8 +41,8 @@ export function useScript<T extends Record<string | symbol, any>>(input: UseScri
4141
// used for devtools integration
4242
if (import.meta.dev && import.meta.client) {
4343
// sync scripts to nuxtApp with debug details
44-
const payload: NuxtAppScript = {
45-
key: (input.key || input.src) as string,
44+
const payload: NuxtDevToolsScriptInstance = {
45+
...options.devtools,
4646
src: input.src,
4747
$script: null as any as VueScriptInstance<T>,
4848
events: [],
@@ -51,7 +51,7 @@ export function useScript<T extends Record<string | symbol, any>>(input: UseScri
5151

5252
function syncScripts() {
5353
nuxtApp._scripts[instance.$script.id] = payload
54-
nuxtApp.hooks.callHook('scripts:updated', { scripts: nuxtApp._scripts as any as Record<string, NuxtAppScript> })
54+
nuxtApp.hooks.callHook('scripts:updated', { scripts: nuxtApp._scripts as any as Record<string, NuxtDevToolsScriptInstance> })
5555
}
5656

5757
if (!nuxtApp._scripts[instance.$script.id]) {
@@ -69,7 +69,7 @@ export function useScript<T extends Record<string | symbol, any>>(input: UseScri
6969
syncScripts()
7070
})
7171
head.hooks.hook('script:instance-fn', (ctx) => {
72-
if (ctx.script.id !== instance.$script.id)
72+
if (ctx.script.id !== instance.$script.id || String(ctx.fn).startsWith('__v_'))
7373
return
7474
// log all events
7575
payload.events.push({

Diff for: src/runtime/types.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ export type NuxtUseScriptOptions<T = any> = Omit<UseScriptOptions<T>, 'trigger'>
5050
* @internal
5151
*/
5252
performanceMarkFeature?: string
53+
/**
54+
* @internal
55+
*/
56+
devtools?: {
57+
/**
58+
* Key used to map to the registry script for Nuxt DevTools.
59+
* @internal
60+
*/
61+
registryKey?: string
62+
/**
63+
* Extra metadata to show with the registry script
64+
* @internal
65+
*/
66+
registryMeta?: Record<string, string>
67+
}
5368
}
5469

5570
export type NuxtUseScriptOptionsSerializable = Omit<NuxtUseScriptOptions, 'use' | 'skipValidation' | 'stub' | 'trigger' | 'eventContext' | 'beforeInit'> & { trigger?: 'client' | 'server' | 'onNuxtReady' }
@@ -74,8 +89,9 @@ export interface ConsentScriptTriggerOptions {
7489
postConsentTrigger?: NuxtUseScriptOptions['trigger']
7590
}
7691

77-
export interface NuxtAppScript {
78-
key: string
92+
export interface NuxtDevToolsScriptInstance {
93+
registryKey?: string
94+
registryMeta?: Record<string, string>
7995
src: string
8096
$script: VueScriptInstance<any>
8197
events: {

Diff for: src/runtime/utils.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,34 @@ export function scriptRuntimeConfig<T extends keyof ScriptRegistry>(key: T) {
3636
return ((useRuntimeConfig().public.scripts || {}) as ScriptRegistry)[key]
3737
}
3838

39-
export function useRegistryScript<T extends Record<string | symbol, any>, O extends ObjectSchema<any, any> = EmptyOptionsSchema>(key: keyof ScriptRegistry | string, optionsFn: OptionsFn<O>, _userOptions?: RegistryScriptInput<O>): T & {
39+
export function useRegistryScript<T extends Record<string | symbol, any>, O extends ObjectSchema<any, any> = EmptyOptionsSchema>(registryKey: keyof ScriptRegistry | string, optionsFn: OptionsFn<O>, _userOptions?: RegistryScriptInput<O>): T & {
4040
$script: Promise<T> & VueScriptInstance<T>
4141
} {
42-
const scriptConfig = scriptRuntimeConfig(key as keyof ScriptRegistry)
42+
const scriptConfig = scriptRuntimeConfig(registryKey as keyof ScriptRegistry)
4343
const userOptions = Object.assign(_userOptions || {}, typeof scriptConfig === 'object' ? scriptConfig : {})
4444
const options = optionsFn(userOptions)
4545

46-
const scriptInput = defu(userOptions.scriptInput, options.scriptInput, { key }) as any as UseScriptInput
46+
const scriptInput = defu(userOptions.scriptInput, options.scriptInput, { key: registryKey }) as any as UseScriptInput
4747
const scriptOptions = Object.assign(userOptions?.scriptOptions || {}, options.scriptOptions || {})
48+
if (import.meta.dev) {
49+
scriptOptions.devtools = defu(scriptOptions.devtools, { registryKey })
50+
if (options.schema) {
51+
const registryMeta: Record<string, string> = {}
52+
for (const k in options.schema.entries) {
53+
if (options.schema.entries[k].type !== 'optional') {
54+
registryMeta[k] = String(userOptions[k as any as keyof typeof userOptions])
55+
}
56+
}
57+
scriptOptions.devtools.registryMeta = registryMeta
58+
}
59+
}
4860
const init = scriptOptions.beforeInit
4961
scriptOptions.beforeInit = () => {
5062
// a manual trigger also means it was disabled by nuxt.config
5163
if (import.meta.dev && !scriptOptions.skipValidation && options.schema) {
5264
// overriding the src will skip validation
5365
if (!userOptions.scriptInput?.src) {
54-
validateScriptInputSchema(key, options.schema, userOptions)
66+
validateScriptInputSchema(registryKey, options.schema, userOptions)
5567
}
5668
}
5769
// avoid clearing the user beforeInit

Diff for: types.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
declare module '#app' {
44
interface NuxtApp {
5-
_scripts: Record<string, (import('#nuxt-scripts').NuxtAppScript)>
5+
_scripts: Record<string, (import('#nuxt-scripts').NuxtDevToolsScriptInstance)>
66
}
77
interface RuntimeNuxtHooks {
8-
'scripts:updated': (ctx: { scripts: Record<string, (import('#nuxt-scripts').NuxtAppScript)> }) => void | Promise<void>
8+
'scripts:updated': (ctx: { scripts: Record<string, (import('#nuxt-scripts').NuxtDevToolsScriptInstance)> }) => void | Promise<void>
99
}
1010
}
1111

0 commit comments

Comments
 (0)