Skip to content

Commit

Permalink
feat(server-routes): read routes from nitro (#286)
Browse files Browse the repository at this point in the history
Co-authored-by: arashsheyda <sheidaeearash1999@gmail.com>
  • Loading branch information
antfu and arashsheyda committed Jun 22, 2023
1 parent 4dd84c4 commit 2cf46b0
Show file tree
Hide file tree
Showing 15 changed files with 161 additions and 122 deletions.
3 changes: 1 addition & 2 deletions packages/devtools-kit/src/_types/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ export interface RouteInfo extends Pick<RouteRecordNormalized, 'name' | 'path' |
export interface ServerRouteInfo {
route: string
filepath: string
path: string
method?: string
type: 'api' | 'route'
type: 'api' | 'route' | 'runtime'
}

export interface Payload {
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools-kit/src/_types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface ServerFunctions {
getServerHooks(): HookInfo[]
getServerLayouts(): NuxtLayout[]
getStaticAssets(): Promise<AssetInfo[]>
getServerRoutes(): Promise<ServerRouteInfo[]>
getServerRoutes(): ServerRouteInfo[]
getServerApp(): NuxtApp | undefined

// Options
Expand Down
38 changes: 38 additions & 0 deletions packages/devtools/client/components/ServerRouteListItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { ServerRouteInfo } from '~/../src/types'
withDefaults(defineProps<{
item: ServerRouteInfo
selected: any
divider?: boolean
}>(), {
divider: true,
})
</script>

<template>
<div>
<NuxtLink
flex="~ gap-2" items-center hover-bg-active px2 py1
:class="[{ 'bg-active': selected?.route === item.route }]"
:to="{ query: { path: item.route } }"
>
<div w-12 flex-none text-left>
<Badge
:class="getRequestMethodClass(item.method || '*')"
v-text="(item.method || '*').toUpperCase()"
/>
</div>
<span flex-auto font-mono text-sm>{{ item.route }}</span>
<Badge
v-if="item.type === 'runtime'"
flex-none
class="bg-indigo-400:10 text-indigo-400"
title="added at runtime"
>
runtime
</Badge>
</NuxtLink>
<div v-if="divider" x-divider />
</div>
</template>
52 changes: 25 additions & 27 deletions packages/devtools/client/pages/modules/server-routes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,51 +23,49 @@ const fuse = computed(() => new Fuse(serverRoutes.value || [], {
shouldSort: true,
}))
const selected = computed(() => serverRoutes.value?.find(i => i.path === vueRoute.query?.path))
const showRuntime = ref(false)
const selected = computed(() => serverRoutes.value?.find(i => i.route === vueRoute.query?.path))
const search = ref('')
const filtered = computed(() => {
if (!serverRoutes.value)
return []
if (!search.value)
return serverRoutes.value
return fuse.value.search(search.value).map(i => i.item)
const result = !serverRoutes.value
? []
: !search.value
? serverRoutes.value
: fuse.value.search(search.value).map(i => i.item)
if (!showRuntime.value)
return result.filter(i => i.type !== 'runtime')
return result
})
</script>

<template>
<PanelLeftRight>
<template #left>
<Navbar v-model:search="search" pb2>
<div flex="~ gap1" text-sm op50>
<span v-if="search">{{ filtered.length }} matched · </span>
<span>{{ serverRoutes?.length }} routes in total</span>
<div flex="~ gap1" text-sm>
<span v-if="search" op50>{{ filtered.length }} matched · </span>
<span op50>{{ serverRoutes?.length }} routes in total</span>
<div flex-auto />
<NCheckbox v-model="showRuntime" n="primary sm">
<span op75>Runtime routes</span>
</NCheckbox>
</div>
</Navbar>

<template v-for="item of filtered" :key="item.id">
<NuxtLink
flex="~ gap-2" items-center hover-bg-active px2 py1
:class="[{ 'bg-active': selected?.path === item.path }]"
:to="{ query: { path: item.path } }"
>
<div w-12 flex-none text-right>
<Badge
:class="getRequestMethodClass(item.method || '*')"
title="updates available"
v-text="(item.method || '*').toUpperCase()"
/>
</div>
<span font-mono text-sm>{{ item.route }}</span>
</NuxtLink>
<div x-divider />
</template>
<ServerRouteListItem
v-for="item of filtered"
:key="item.filepath"
:item="item"
:selected="selected"
/>
</template>
<template #right>
<KeepAlive :max="10">
<ServerRouteDetails
v-if="selected"
:key="selected.path"
:key="selected.filepath"
:route="selected"
/>
</KeepAlive>
Expand Down
117 changes: 25 additions & 92 deletions packages/devtools/src/server-rpc/server-routes.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,61 @@
import type { Nitro } from 'nitropack'
import { join, resolve } from 'pathe'
import fg from 'fast-glob'
import { withBase, withLeadingSlash, withoutTrailingSlash } from 'ufo'
import { debounce } from 'perfect-debounce'
import type { NuxtDevtoolsServerContext, ServerFunctions, ServerRouteInfo } from '../types'

export function setupServerRoutesRPC({ nuxt, refresh }: NuxtDevtoolsServerContext) {
let nitro: Nitro

let promiseCache: Promise<ServerRouteInfo[]> | null = null
let cache: ServerRouteInfo[] | null = null

const refreshDebounced = debounce(() => {
promiseCache = null
cache = null
refresh('getServerRoutes')
}, 500)

nuxt.hook('nitro:init', (_) => {
nitro = _
promiseCache = null
cache = null
refresh('getServerRoutes')
})

nuxt.hook('ready', () => {
nitro?.storage.watch((event, key) => {
if (key.startsWith('src:api:'))
if (key.startsWith('src:api:') || key.startsWith('src:routes:'))
refreshDebounced()
})
})

function scan() {
if (promiseCache)
return promiseCache
if (cache)
return cache

promiseCache = (async () => {
cache = (() => {
if (!nitro)
return []
const files = await Promise.all([
scanServerDir(nitro, 'api', scanFileHandler('api')),
scanServerDir(nitro, 'routes', scanFileHandler('route')),
]).then(r => r.flat())
return files
return [
...nitro.scannedHandlers.map(item => ({
route: item.route,
filepath: item.handler,
method: item.method,
type: item.route?.startsWith('/api') ? 'api' : 'route',
})),
...nitro.options.handlers
.filter(item => !item.route?.startsWith('/_nitro') && !item.route?.startsWith('/__nuxt') && !item.middleware)
.map(item => ({
route: item.route,
filepath: item.handler,
method: item.method,
type: 'runtime',
})),
] as ServerRouteInfo[]
})()

return promiseCache
return cache
}

return {
async getServerRoutes() {
getServerRoutes() {
return scan()
},
} satisfies Partial<ServerFunctions>
}

/**
* Ported from Nitropack:
* https://github.com/unjs/nitro/blob/ea5ea881a7bfa74def754d0a7120be4bc5b3ba7d/src/scan.ts
*/

interface FileInfo {
dir: string
path: string
fullPath: string
}

const GLOB_SCAN_PATTERN = '**/*.{ts,mjs,js,cjs}'
const httpMethodRegex = /\.(connect|delete|get|head|options|patch|post|put|trace)/

function scanFileHandler(type: ServerRouteInfo['type'], prefix = '/') {
return (file: FileInfo): ServerRouteInfo => {
let route = file.path
.replace(/\.[A-Za-z]+$/, '')
.replace(/\[\.{3}]/g, '**')
.replace(/\[\.{3}(\w+)]/g, '**:$1')
.replace(/\[(\w+)]/g, ':$1')
route = withLeadingSlash(withoutTrailingSlash(withBase(route, prefix)))

let method
const methodMatch = route.match(httpMethodRegex)
if (methodMatch) {
route = route.slice(0, Math.max(0, methodMatch.index!))
method = methodMatch[1]
}

route = route.replace(/\/index$/, '') || '/'

if (type === 'api')
route = `/api${route}`

return {
filepath: file.fullPath,
path: file.path,
route,
type,
method,
}
}
}

async function scanServerDir(
nitro: Nitro,
name: string,
mapper: (file: FileInfo) => ServerRouteInfo,
): Promise<ServerRouteInfo[]> {
const dirs = nitro.options.scanDirs.map(dir => join(dir, name))
const files = await scanDirs(dirs)
return files.map(f => mapper(f))
}

function scanDirs(dirs: string[]): Promise<FileInfo[]> {
return Promise.all(
dirs.map(async (dir) => {
const fileNames = await fg(GLOB_SCAN_PATTERN, {
cwd: dir,
dot: true,
})
return fileNames
.map((fileName) => {
return {
dir,
path: fileName,
fullPath: resolve(dir, fileName),
}
})
.sort((a, b) => a.path.localeCompare(b.path))
}),
).then(r => r.flat())
}
2 changes: 2 additions & 0 deletions playgrounds/tab-server-route/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false
8 changes: 8 additions & 0 deletions playgrounds/tab-server-route/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script setup lang="ts">
</script>

<template>
<div px10 text-6xl>
Server Routes [Playground]
</div>
</template>
15 changes: 15 additions & 0 deletions playgrounds/tab-server-route/modules/custom-module/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { addServerHandler, createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
async setup(_) {
const { resolve } = createResolver(import.meta.url)

const runtime = './runtime'

addServerHandler({
route: '/api/hello-from-custom-module',
method: 'get',
handler: resolve(runtime, 'server/api/hello'),
})
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineEventHandler(() => {
return {
message: 'Hello from custom module',
}
})
7 changes: 7 additions & 0 deletions playgrounds/tab-server-route/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: [
'../../local',
'./modules/custom-module',
],
})
13 changes: 13 additions & 0 deletions playgrounds/tab-server-route/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview"
},
"devDependencies": {
"@types/node": "^20.2.5",
"nuxt": "^3.5.2"
}
}
5 changes: 5 additions & 0 deletions playgrounds/tab-server-route/server/api/hello.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineEventHandler(() => {
return {
message: 'Hello from playground!',
}
})
3 changes: 3 additions & 0 deletions playgrounds/tab-server-route/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}
4 changes: 4 additions & 0 deletions playgrounds/tab-server-route/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2cf46b0

Please sign in to comment.