Files
vue-menu-layer/tests/render.ts
Jakub Zych d980f4cf2f 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
2026-10-12 18:19:53 +02:00

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, '')
}