- typed client and useMenu/useMenuApi (SSR, menu:<code>:<locale> key, server and public bases, x-forwarded-for forwarded also on cached renders) - MenuNav and recursive MenuNavItem: WAI disclosure navigation, URL allowlist with a text fallback, rel on _blank, active link and trail, mobile toggle, Esc / focus-out / outside pointer / route change closing - menu tokens and base CSS, README - node tests (link rules, client, server-rendered NavItem) and a playground SSR smoke against a fixture API
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
// Test helper: compiles app/components/Menu/NavItem.vue with Vue's SFC
|
|
// compiler and renders it with the server renderer. NuxtLink is stubbed as
|
|
// a plain anchor (it renders one for a path in the app too), the component
|
|
// resolves itself for the recursion, and the returned HTML has no hydration
|
|
// comments.
|
|
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
import { compileScript, parse } from 'vue/compiler-sfc'
|
|
import { createSSRApp, defineComponent, h, type Component } from 'vue'
|
|
import { renderToString } from 'vue/server-renderer'
|
|
import type { MenuEntry } from '../app/utils/types.ts'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
const source = join(here, '..', 'app', 'components', 'Menu', 'NavItem.vue')
|
|
|
|
let compiled: Promise<Component> | null = null
|
|
|
|
async function compileNavItem(): Promise<Component> {
|
|
const { descriptor } = parse(readFileSync(source, 'utf8'), { filename: source })
|
|
const script = compileScript(descriptor, { id: 'navitem', inlineTemplate: true, genDefaultAs: '__sfc__' })
|
|
// The compiled module keeps the relative imports, so it is written next to
|
|
// the source, imported (Node strips the types) and removed.
|
|
const out = join(dirname(source), `.navitem-${process.pid}.compiled.ts`)
|
|
writeFileSync(out, `${script.content}\nexport default __sfc__\n`)
|
|
try {
|
|
const mod = (await import(pathToFileURL(out).href)) as { default: Component }
|
|
return mod.default
|
|
} finally {
|
|
rmSync(out, { force: true })
|
|
}
|
|
}
|
|
|
|
const NuxtLink = defineComponent({
|
|
props: { to: { type: String, required: true } },
|
|
setup(props, { slots, attrs }) {
|
|
return () => h('a', { href: props.to, ...attrs }, slots.default?.())
|
|
},
|
|
})
|
|
|
|
export async function renderNavItem(opts: { entry: MenuEntry; routePath: string; open?: boolean }): Promise<string> {
|
|
compiled ??= compileNavItem()
|
|
const NavItem = await compiled
|
|
const app = createSSRApp({
|
|
render: () =>
|
|
h(NavItem, {
|
|
entry: opts.entry,
|
|
depth: 0,
|
|
idBase: 'm-0',
|
|
open: opts.open ?? false,
|
|
routePath: opts.routePath,
|
|
host: 'site.test',
|
|
submenuLabel: 'Show submenu: :title',
|
|
}),
|
|
})
|
|
app.component('NuxtLink', NuxtLink)
|
|
app.component('MenuNavItem', NavItem)
|
|
// Hydration markers (<!--[-->, <!--v-if-->) are dropped for readable assertions.
|
|
return (await renderToString(app)).replace(/<!--[^>]*-->/g, '')
|
|
}
|