Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,6 @@ export default defineNuxtConfig({
// Registered here, not scanned from `server/middleware`, because nuxt-ai-ready
// claims every `.md` path and scanned middleware runs after a module's.
handlers: [
// First, so content negotiation also leaves Skill file URLs alone.
{ middleware: true, handler: '~~/server/handlers/skill-file-page.ts' },
{ middleware: true, handler: '~~/server/handlers/skill-md-probe.ts' },
{ middleware: true, handler: '~~/server/handlers/content-negotiation.ts' },
],
Expand Down
1 change: 1 addition & 0 deletions server/handlers/skill-file-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { isSkillFilePagePath } from '#shared/skill-asset-path'
*
* Content negotiation skips the same requests. The file is already the
* content, so no Markdown twin exists to redirect to.
* The request plugin calls this before the module caches its routing decision.
*/
export default defineEventHandler((event) => {
if (isSkillFilePagePath(event.path))
Expand Down
7 changes: 7 additions & 0 deletions server/plugins/skill-file-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import claimSkillFilePage from '../handlers/skill-file-page'

// nuxt-ai-ready prepends h3 middleware that caches the request's Markdown
// decision. Nitro's request hook runs before that middleware can claim a file.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', claimSkillFilePage)
})
49 changes: 49 additions & 0 deletions test/unit/skill-file-page.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { H3Event } from 'h3'
import { createApp, defineEventHandler, getRequestHeader, toWebHandler } from 'h3'
import { describe, expect, it, vi } from 'vitest'
import registerPlugin from '../../server/plugins/skill-file-page'

vi.hoisted(() => {
vi.stubGlobal('defineEventHandler', (handler: unknown) => handler)
vi.stubGlobal('defineNitroPlugin', (plugin: unknown) => plugin)
})

async function request(path: string) {
let onRequest: (event: H3Event) => void = () => {}
const app = createApp({ onRequest: event => onRequest(event) })
const nitro = {
hooks: {
hook(_name: string, callback: typeof onRequest) {
onRequest = callback
},
},
}
registerPlugin(nitro as unknown as Parameters<typeof registerPlugin>[0])
// A module can prepend its own h3 middleware. It must see the guard before
// it decides whether a .md URL names a file or a page's Markdown twin.
app.use(defineEventHandler(event => ({
path: event.path,
internal: getRequestHeader(event, 'x-ai-ready-internal') ?? null,
})))
return toWebHandler(app)(new Request(`http://localhost${path}`))
}

describe('skill file request ownership', () => {
it.each([
'/gh/huggingface/skills/huggingface-lora-space-builder/-/references/adapting-to-the-lora.md',
'/gh/anthropics/skills/pdf/-/reference.md?section=setup',
'/gh/anthropics/skills/pdf/-/scripts/check_bounding_boxes.py',
])('claims %s before h3 middleware runs', async (path) => {
const response = await request(path)
expect(await response.json()).toEqual({ path, internal: '1' })
})

it.each([
'/gh/anthropics/skills/pdf.md',
'/gh/anthropics/skills/pdf',
'/index.md',
])('leaves the page representation for %s to the module', async (path) => {
const response = await request(path)
expect(await response.json()).toEqual({ path, internal: null })
})
})
Loading