feat: MenuNav layer for the SummerCMS menu API
- 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
This commit is contained in:
60
tests/render.ts
Normal file
60
tests/render.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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, '')
|
||||
}
|
||||
Reference in New Issue
Block a user