Skip to content

Commit

Permalink
fix(devtools): support multiple of the same registry scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Jul 24, 2024
1 parent 2eedd4a commit 8794d74
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 46 deletions.
111 changes: 79 additions & 32 deletions client/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,42 @@ import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
import { registry } from '../src/registry'
import { devtools, getScriptSize, humanFriendlyTimestamp, reactive, ref, urlToOrigin } from '#imports'
import { loadShiki } from '~/composables/shiki'
import { msToHumanReadable } from '~/utils/formatting'
const scriptRegistry = registry(s => s)
await loadShiki()
const scripts = ref({})
const scriptSizes = reactive({})
const scriptSizes = reactive<Record<string, string>>({})
function syncScripts(_scripts: any[]) {
scripts.value = { ..._scripts }
// check if the script size has been set, if not set it
for (const key in _scripts) {
if (!scriptSizes[key]) {
getScriptSize(_scripts[key].src).then((size) => {
scriptSizes[key] = size
}).catch(() => {
scriptSizes[key] = 0
})
}
}
// augment the scripts with registry
scripts.value = Object.fromEntries(
Object.entries({ ..._scripts })
.map(([key, script]) => {
script.registry = scriptRegistry.find(s => titleToCamelCase(s.label) === script.registryKey)
if (script.registry) {
const kebabCaseLabel = script.registry.label.toLowerCase().replace(/ /g, '-')
script.docs = `https://scripts.nuxt.com/scripts/${script.registry.category}/${kebabCaseLabel}`
}
const loadingAt = script.events.find(e => e.status === 'loading')?.at || 0
const loadedAt = script.events.find(e => e.status === 'loaded')?.at || 0
if (loadingAt && loadedAt) {
script.loadTime = msToHumanReadable(loadedAt - loadingAt)
}
const scriptSizeKey = script.src
if (!scriptSizes[scriptSizeKey]) {
getScriptSize(script.src).then((size) => {
scriptSizes[scriptSizeKey] = size
script.size = size
}).catch(() => {
script.size = ''
scriptSizes[scriptSizeKey] = ''
})
}
return [key, script]
}),
)
}
function titleToCamelCase(s: string) {
Expand All @@ -31,9 +48,7 @@ function titleToCamelCase(s: string) {
return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
}).join('')
}
function resolveScriptRegistry(id: string) {
return scriptRegistry.find(s => titleToCamelCase(s.label) === id)
}
const version = ref(null)
onDevtoolsClientConnected(async (client) => {
devtools.value = client.devtools
Expand All @@ -44,6 +59,16 @@ onDevtoolsClientConnected(async (client) => {
syncScripts(client.host.nuxt._scripts)
})
const tab = ref('scripts')
function viewDocs(docs: string) {
tab.value = 'docs'
setTimeout(() => {
const iframe = document.querySelector('iframe')
if (iframe) {
iframe.src = docs
}
}, 100)
}
</script>

<template>
Expand Down Expand Up @@ -116,14 +141,6 @@ const tab = ref('scripts')
>
</label>
</fieldset>
<!-- <VTooltip> -->
<!-- <button text-lg="" type="button" class="n-icon-button n-button n-transition n-disabled:n-disabled"> -->
<!-- <NIcon icon="carbon:reset" class="group-hover:text-green-500" /> -->
<!-- </button> -->
<!-- <template #popper> -->
<!-- Refresh -->
<!-- </template> -->
<!-- </VTooltip> -->
</div>
<div class="items-center space-x-3 hidden lg:flex">
<div class="opacity-80 text-sm">
Expand All @@ -147,19 +164,49 @@ const tab = ref('scripts')
<div class="space-y-3">
<OSectionBlock v-for="(script, id) in scripts" :key="id" class="w-full">
<template #text>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-4">
<a class="text-lg font-bold flex gap-2 items-center font-mono" :title="script.src" target="_blank" :href="script.src">
<div v-if="resolveScriptRegistry(id)" class="flex items-center max-w-10 h-6" v-html="resolveScriptRegistry(id).logo" />
<div class="flex items-center justify-between w-full gap-7">
<div class="flex items-center gap-7">
<div class="flex items-center gap-1">
<div v-if="script.registry" class="flex items-center max-w-6 h-6" v-html="script.registry.logo" />
<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">
<div>{{ resolveScriptRegistry(id)?.label || script.key }}</div>
</a>
<div class="opacity-70">
<div>
<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">
<div>
{{ script.registry?.label || script.key }}
</div>
</a>
<div class="flex flex-items-center gap-3">
<template v-if="script.docs">
<button type="button" class="ml-2 opacity-50 hover:opacity-70 transition ml-1 text-xs underline" @click="viewDocs(script.docs)">
View docs
</button>
</template>
<div v-for="k in Object.keys(script.registryMeta)" :key="k" class="text-xs text-gray-500">
<span class="capitalize">{{ k }}</span>: {{ script.registryMeta[k] }}
</div>
</div>
</div>
</div>
</div>
<div>
<div class="opacity-70 text-xs">
Status
</div>
<div class="capitalize">
{{ script.$script.status.value }}
</div>
<div v-if="scriptSizes[script.key]">
{{ scriptSizes[script.key] }}
</div>
<div v-if="scriptSizes[script.src]">
<div class="opacity-70 text-xs">
Size
</div>
<div>{{ scriptSizes[script.src] }}</div>
</div>
<div v-if="script.loadTime">
<div class="opacity-70 text-xs">
Time to loaded
</div>
<div>{{ script.loadTime }}</div>
</div>
<div>
<NButton v-if="script.$script.status === 'awaitingLoad'" @click="script.$script.load()">
Expand Down
2 changes: 1 addition & 1 deletion client/components/OSectionBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function onToggle(e: any) {

<template>
<details :open="open" @toggle="onToggle">
<summary class="cursor-pointer select-none n-bg-active hover:bg-active p4 rounded transition-all" :class="collapse ? '' : 'pointer-events-none'">
<summary class="cursor-pointer select-none n-bg-active hover:bg-active px-2 py-2 rounded transition-all" :class="collapse ? '' : 'pointer-events-none'">
<NIconTitle :icon="icon" :text="text" text-xl transition :class="[open ? 'op100' : 'op60', headerClass]">
<div>
<div text-base>
Expand Down
10 changes: 10 additions & 0 deletions client/utils/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ export function urlToOrigin(url: string) {
return new URL(url).origin
return url
}

export function msToHumanReadable(ms: number) {
if (ms < 1000) {
return ms + 'ms'
}
if (ms < 60000) {
return (ms / 1000).toFixed(2) + 's'
}
return (ms / 60000).toFixed(2) + 'm'
}
10 changes: 5 additions & 5 deletions src/runtime/composables/useScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { hashCode } from '@unhead/shared'
import { defu } from 'defu'
import { useScript as _useScript } from '@unhead/vue'
import { injectHead, onNuxtReady, useNuxtApp, useRuntimeConfig, reactive } from '#imports'
import type { NuxtAppScript, NuxtUseScriptOptions } from '#nuxt-scripts'
import type { NuxtDevToolsScriptInstance, NuxtUseScriptOptions } from '#nuxt-scripts'

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

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

if (!nuxtApp._scripts[instance.$script.id]) {
Expand All @@ -69,7 +69,7 @@ export function useScript<T extends Record<string | symbol, any>>(input: UseScri
syncScripts()
})
head.hooks.hook('script:instance-fn', (ctx) => {
if (ctx.script.id !== instance.$script.id)
if (ctx.script.id !== instance.$script.id || String(ctx.fn).startsWith('__v_'))
return
// log all events
payload.events.push({
Expand Down
20 changes: 18 additions & 2 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ export type NuxtUseScriptOptions<T = any> = Omit<UseScriptOptions<T>, 'trigger'>
* @internal
*/
performanceMarkFeature?: string
/**
* @internal
*/
devtools?: {
/**
* Key used to map to the registry script for Nuxt DevTools.
* @internal
*/
registryKey?: string
/**
* Extra metadata to show with the registry script
* @internal
*/
registryMeta?: Record<string, string>
}
}

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

export interface NuxtAppScript {
key: string
export interface NuxtDevToolsScriptInstance {
registryKey?: string
registryMeta?: Record<string, string>
src: string
$script: VueScriptInstance<any>
events: {
Expand Down
20 changes: 16 additions & 4 deletions src/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,34 @@ export function scriptRuntimeConfig<T extends keyof ScriptRegistry>(key: T) {
return ((useRuntimeConfig().public.scripts || {}) as ScriptRegistry)[key]
}

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 & {
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 & {
$script: Promise<T> & VueScriptInstance<T>
} {
const scriptConfig = scriptRuntimeConfig(key as keyof ScriptRegistry)
const scriptConfig = scriptRuntimeConfig(registryKey as keyof ScriptRegistry)
const userOptions = Object.assign(_userOptions || {}, typeof scriptConfig === 'object' ? scriptConfig : {})
const options = optionsFn(userOptions)

const scriptInput = defu(userOptions.scriptInput, options.scriptInput, { key }) as any as UseScriptInput
const scriptInput = defu(userOptions.scriptInput, options.scriptInput, { key: registryKey }) as any as UseScriptInput
const scriptOptions = Object.assign(userOptions?.scriptOptions || {}, options.scriptOptions || {})
if (import.meta.dev) {
scriptOptions.devtools = defu(scriptOptions.devtools, { registryKey })
if (options.schema) {
const registryMeta: Record<string, string> = {}
for (const k in options.schema.entries) {
if (options.schema.entries[k].type !== 'optional') {
registryMeta[k] = String(userOptions[k as any as keyof typeof userOptions])
}
}
scriptOptions.devtools.registryMeta = registryMeta
}
}
const init = scriptOptions.beforeInit
scriptOptions.beforeInit = () => {
// a manual trigger also means it was disabled by nuxt.config
if (import.meta.dev && !scriptOptions.skipValidation && options.schema) {
// overriding the src will skip validation
if (!userOptions.scriptInput?.src) {
validateScriptInputSchema(key, options.schema, userOptions)
validateScriptInputSchema(registryKey, options.schema, userOptions)
}
}
// avoid clearing the user beforeInit
Expand Down
4 changes: 2 additions & 2 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

declare module '#app' {
interface NuxtApp {
_scripts: Record<string, (import('#nuxt-scripts').NuxtAppScript)>
_scripts: Record<string, (import('#nuxt-scripts').NuxtDevToolsScriptInstance)>
}
interface RuntimeNuxtHooks {
'scripts:updated': (ctx: { scripts: Record<string, (import('#nuxt-scripts').NuxtAppScript)> }) => void | Promise<void>
'scripts:updated': (ctx: { scripts: Record<string, (import('#nuxt-scripts').NuxtDevToolsScriptInstance)> }) => void | Promise<void>
}
}

Expand Down

0 comments on commit 8794d74

Please sign in to comment.