Skip to content

Commit

Permalink
refactor(devtools): override nuxt-root
Browse files Browse the repository at this point in the history
  • Loading branch information
romhml committed Sep 18, 2024
1 parent f55a11c commit 72a6ca2
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 2,886 deletions.
2,882 changes: 5 additions & 2,877 deletions playground/app/app.config.ts

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions playground/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@ defineShortcuts({
</script>

<template>
<!-- TODO: Find a way to include the page without the added template from app.vue and layouts -->
<div v-if="$route.name === 'ui-devtools'">
<NuxtPage />
</div>
<UApp v-else :toaster="appConfig.toaster">
<UApp :toaster="appConfig.toaster">
<div class="h-screen w-screen overflow-hidden flex min-h-0 bg-white dark:bg-gray-900" vaul-drawer-wrapper>
<UNavigationMenu :items="items" orientation="vertical" class="border-r border-gray-200 dark:border-gray-800 overflow-y-auto w-48 p-4" />

Expand Down
37 changes: 37 additions & 0 deletions playground/app/components/island-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { defineAsyncComponent } from 'vue'
import { createVNode, defineComponent, onErrorCaptured } from 'vue'

import { injectHead } from '@unhead/vue'

// @ts-expect-error virtual file
import { islandComponents } from '#build/components.islands.mjs'

export default defineComponent({
name: 'IslandRenderer',
props: {
context: {
type: Object as () => { name: string, props?: Record<string, any> },
required: true
}
},
setup(props) {
// reset head - we don't want to have any head tags from plugin or anywhere else.
const head = injectHead()
head.headEntries().splice(0, head.headEntries().length)

const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>

if (!component) {
throw createError({
statusCode: 404,
statusMessage: `Island component not found: ${props.context.name}`
})
}

onErrorCaptured((e) => {
console.log(e)
})

return () => createVNode(component || 'span', { ...props.context.props, 'data-island-uid': '' })
}
})
66 changes: 66 additions & 0 deletions playground/app/components/nuxt-root.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<template>
<Suspense @resolve="onResolve">
<div v-if="abortRender" />
<ErrorComponent
v-else-if="error"
:error="error"
/>
<IslandRenderer
v-else-if="islandContext"
:context="islandContext"
/>
<component
:is="SingleRenderer"
v-else-if="SingleRenderer"
/>
<AppComponent v-else />
</Suspense>
</template>

<script setup>
import { defineAsyncComponent, onErrorCaptured, onServerPrefetch, provide } from 'vue'
import AppComponent from '#build/app-component.mjs'
import ErrorComponent from '#build/error-component.mjs'
// @ts-expect-error virtual file
import { componentIslands } from '#build/nuxt.config.mjs'
const IslandRenderer = import.meta.server && componentIslands
? defineAsyncComponent(() => import('./island-renderer').then(r => r.default || r))
: () => null
const nuxtApp = useNuxtApp()
const onResolve = nuxtApp.deferHydration()
if (import.meta.client && nuxtApp.isHydrating) {
const removeErrorHook = nuxtApp.hooks.hookOnce('app:error', onResolve)
useRouter().beforeEach(removeErrorHook)
}
const url = import.meta.server ? nuxtApp.ssrContext.url : window.location.pathname
const SingleRenderer = import.meta.test && import.meta.dev && import.meta.server && url.startsWith('/__nuxt_component_test__/') && defineAsyncComponent(() => import('#build/test-component-wrapper.mjs')
.then(r => r.default(import.meta.server ? url : window.location.href)))
// Inject default route (outside of pages) as active route
provide('route', useRoute())
// vue:setup hook
const results = nuxtApp.hooks.callHookWith(hooks => hooks.map(hook => hook()), 'vue:setup')
if (import.meta.dev && results && results.some(i => i && 'then' in i)) {
console.error('[nuxt] Error in `vue:setup`. Callbacks must be synchronous.')
}
// error handling
const error = useError()
// render an empty <div> when plugins have thrown an error but we're not yet rendering the error page
const abortRender = import.meta.server && error.value && !nuxtApp.ssrContext.error
onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
const p = nuxtApp.runWithContext(() => showError(err))
onServerPrefetch(() => p)
return false // suppress error from breaking render
}
})
// Component islands context
const islandContext = import.meta.server && nuxtApp.ssrContext.islandContext
</script>
File renamed without changes.
37 changes: 37 additions & 0 deletions src/devtools/island-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { defineAsyncComponent } from 'vue'
import { createVNode, defineComponent, onErrorCaptured } from 'vue'

import { injectHead } from '@unhead/vue'

// @ts-expect-error virtual file
import { islandComponents } from '#build/components.islands.mjs'

export default defineComponent({
name: 'IslandRenderer',
props: {
context: {
type: Object as () => { name: string, props?: Record<string, any> },
required: true
}
},
setup(props) {
// reset head - we don't want to have any head tags from plugin or anywhere else.
const head = injectHead()
head.headEntries().splice(0, head.headEntries().length)

const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>

if (!component) {
throw createError({
statusCode: 404,
statusMessage: `Island component not found: ${props.context.name}`
})
}

onErrorCaptured((e) => {
console.log(e)
})

return () => createVNode(component || 'span', { ...props.context.props, 'data-island-uid': '' })
}
})
69 changes: 69 additions & 0 deletions src/devtools/nuxt-root.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<Suspense @resolve="onResolve">
<div v-if="abortRender" />
<ErrorComponent
v-else-if="error"
:error="error"
/>
<IslandRenderer
v-else-if="islandContext"
:context="islandContext"
/>
<component
:is="SingleRenderer"
v-else-if="SingleRenderer"
/>
<Devtools v-else-if="url === '/_ui/devtools'" />
<AppComponent v-else />
</Suspense>
</template>

<script setup>
import { defineAsyncComponent, onErrorCaptured, onServerPrefetch, provide } from 'vue'
import AppComponent from '#build/app-component.mjs'
import ErrorComponent from '#build/error-component.mjs'
// @ts-expect-error virtual file
import { componentIslands } from '#build/nuxt.config.mjs'
import Devtools from './components/Devtools.vue'
const IslandRenderer = import.meta.server && componentIslands
? defineAsyncComponent(() => import('./island-renderer').then(r => r.default || r))
: () => null
const nuxtApp = useNuxtApp()
const onResolve = nuxtApp.deferHydration()
if (import.meta.client && nuxtApp.isHydrating) {
const removeErrorHook = nuxtApp.hooks.hookOnce('app:error', onResolve)
useRouter().beforeEach(removeErrorHook)
}
const url = import.meta.server ? nuxtApp.ssrContext.url : window.location.pathname
const SingleRenderer = import.meta.test && import.meta.dev && import.meta.server && url.startsWith('/__nuxt_component_test__/') && defineAsyncComponent(() => import('#build/test-component-wrapper.mjs')
.then(r => r.default(import.meta.server ? url : window.location.href)))
// Inject default route (outside of pages) as active route
provide('route', useRoute())
// vue:setup hook
const results = nuxtApp.hooks.callHookWith(hooks => hooks.map(hook => hook()), 'vue:setup')
if (import.meta.dev && results && results.some(i => i && 'then' in i)) {
console.error('[nuxt] Error in `vue:setup`. Callbacks must be synchronous.')
}
// error handling
const error = useError()
// render an empty <div> when plugins have thrown an error but we're not yet rendering the error page
const abortRender = import.meta.server && error.value && !nuxtApp.ssrContext.error
onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
const p = nuxtApp.runWithContext(() => showError(err))
onServerPrefetch(() => p)
return false // suppress error from breaking render
}
})
// Component islands context
const islandContext = import.meta.server && nuxtApp.ssrContext.islandContext
</script>
File renamed without changes.
12 changes: 8 additions & 4 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { defu } from 'defu'
import { createResolver, defineNuxtModule, addComponentsDir, addImportsDir, addVitePlugin, addPlugin, installModule, extendPages, addServerHandler, hasNuxtModule } from '@nuxt/kit'
import { addTemplates } from './templates'
import icons from './theme/icons'

import { addCustomTab } from '@nuxt/devtools-kit'

export type * from './runtime/types'
Expand Down Expand Up @@ -121,20 +120,25 @@ export default defineNuxtModule<ModuleOptions>({
addTemplates(options, nuxt)

if (nuxt.options.dev) {
nuxt.hook('app:resolve', (app) => {
app.rootComponent = resolve('./devtools/nuxt-root.vue')
})

addServerHandler({
route: '/_ui/config',
handler: resolve('./runtime/devtools/server/config.post.ts'),
handler: resolve('./devtools/server/config.post.ts'),
method: 'POST'
})

extendPages((pages) => {
pages.unshift({
name: 'ui-devtools',
path: '/_ui/devtools',
file: resolve('runtime/devtools/pages/index.vue')
path: '/_ui/devtools'
})
})

nuxt.options.nitro.routeRules['_ui/**'] = { ssr: false }

addCustomTab({
name: 'nuxt-ui',
title: 'Nuxt UI',
Expand Down

0 comments on commit 72a6ca2

Please sign in to comment.