Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pre / post parsing hooks #2925

Open
wants to merge 6 commits into
base: v3
Choose a base branch
from
Open
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
53 changes: 53 additions & 0 deletions docs/content/docs/7.advanced/5.hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Hooks
description: Modify your content using Nuxt build time hooks
navigation:
title: Hooks
---

## `content:file:beforeParse`{lang="ts"}

This hook is called before the content is parsed.

It can be used to modify the raw content from a `file` before it is transformed
or modify the transform options.

```ts
export interface FileBeforeParseHook {
collection: ResolvedCollection
file: ContentFile
parserOptions: TransformContentOptions
}
```

```ts
export default defineNuxtConfig({
hooks: {
'content:file:beforeParse'(ctx) {
// ...
}
}
})
```

## `content:file:afterParse`{lang="ts"}

This hook is called after the content is parsed and before it is saved to the database.

```ts
export interface FileAfterParseHook {
collection: ResolvedCollection
file: ContentFile
content: ParsedContentFile
}
```

```ts
export default defineNuxtConfig({
hooks: {
'content:file:afterParse'(ctx) {
// ...
}
}
})
```
22 changes: 21 additions & 1 deletion src/types/module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { BuiltinLanguage as ShikiLang, BuiltinTheme as ShikiTheme, LanguageRegistration, ThemeRegistrationAny, ThemeRegistrationRaw } from 'shiki'
import type { ListenOptions } from 'listhen'
import type { GitInfo } from '../utils/git'
import type { MarkdownPlugin } from './content'
import type { ContentFile, MarkdownPlugin, TransformContentOptions } from './content'
import type { PathMetaOptions } from './path-meta'
import type { ResolvedCollection } from './collection'

export interface D1DatabaseConfig {
type: 'd1'
Expand Down Expand Up @@ -187,3 +188,22 @@ export interface PublicRuntimeConfig {
iframeMessagingAllowedOrigins?: string
}
}

// TODO improve types
export type ParsedContentFile = Record<string, unknown>

export interface FileBeforeParseHook {
collection: ResolvedCollection
file: ContentFile
parserOptions: TransformContentOptions
}
export interface FileAfterParseHook {
collection: ResolvedCollection
file: ContentFile
content: ParsedContentFile
}

export interface ModuleHooks {
'content:file:beforeParse': (hook: FileBeforeParseHook) => void | Promise<void>
'content:file:afterParse': (hook: FileAfterParseHook) => void | Promise<void>
}
3 changes: 2 additions & 1 deletion src/utils/collection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ZodObject, ZodOptionalDef, ZodRawShape, ZodStringDef, ZodType } from 'zod'
import type { Collection, ResolvedCollection, CollectionSource, DefinedCollection, ResolvedCollectionSource } from '../types/collection'
import { getOrderedSchemaKeys } from '../runtime/internal/schema'
import type { ParsedContentFile } from '../types'
import { defineLocalSource, defineGitHubSource } from './source'
import { metaSchema, pageSchema } from './schema'
import type { ZodFieldType } from './zod'
Expand Down Expand Up @@ -98,7 +99,7 @@ function resolveSource(source: string | CollectionSource | CollectionSource[] |
}

// Convert collection data to SQL insert statement
export function generateCollectionInsert(collection: ResolvedCollection, data: Record<string, unknown>): string[] {
export function generateCollectionInsert(collection: ResolvedCollection, data: ParsedContentFile): string[] {
const fields: string[] = []
const values: Array<string | number | boolean> = []
const sortedKeys = getOrderedSchemaKeys((collection.extendedSchema).shape)
Expand Down
19 changes: 14 additions & 5 deletions src/utils/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import type { Nuxt } from '@nuxt/schema'
import { defu } from 'defu'
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
import { visit } from 'unist-util-visit'
import type { ResolvedCollection } from '../../types/collection'
import type { ModuleOptions } from '../../types/module'
import type {
ResolvedCollection,
} from '../../types/collection'
import type { FileAfterParseHook, FileBeforeParseHook, ModuleOptions } from '../../types/module'
import { transformContent } from './transformers'
import type { ContentFile } from '~/src/types'

Expand Down Expand Up @@ -131,13 +133,17 @@ export async function createParser(collection: ResolvedCollection, nuxt?: Nuxt)
if (file.path && !file.dirname) {
file.dirname = dirname(file.path)
}
const beforeParseCtx: FileBeforeParseHook = { file, collection, parserOptions }
// @ts-expect-error runtime type
await nuxt?.callHook?.('content:file:beforeParse', beforeParseCtx)
const { file: hookedFile, collection: hookedCollection } = beforeParseCtx

const parsedContent = await transformContent(file, parserOptions)
const parsedContent = await transformContent(hookedFile, beforeParseCtx.parserOptions)
const { id: id, ...parsedContentFields } = parsedContent
const result = { id } as typeof collection.extendedSchema._type
const meta = {} as Record<string, unknown>

const collectionKeys = Object.keys(collection.extendedSchema.shape)
const collectionKeys = Object.keys(hookedCollection.extendedSchema.shape)
for (const key of Object.keys(parsedContentFields)) {
if (collectionKeys.includes(key)) {
result[key] = parsedContent[key]
Expand All @@ -160,6 +166,9 @@ export async function createParser(collection: ResolvedCollection, nuxt?: Nuxt)
result.seo.description = result.seo.description || result.description
}

return result
const afterParseCtx: FileAfterParseHook = { file: hookedFile, content: result, collection }
// @ts-expect-error runtime type
await nuxt?.callHook?.('content:file:afterParse', afterParseCtx)
return afterParseCtx.content
}
}
58 changes: 58 additions & 0 deletions test/unit/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest'
import { z } from 'zod'
import { defineCollection } from '../../src/utils'
import { resolveCollection } from '../../src/utils/collection'
import { parseContent } from '../utils/content'
import type { FileAfterParseHook, FileBeforeParseHook } from '../../src/types'

describe('Hooks', () => {
const collection = resolveCollection('hookTest', defineCollection({
type: 'data',
source: 'content/**',
schema: z.object({
body: z.any(),
foo: z.any(),
}),
}))!
it('content:file:beforeParse', async () => {
let hookCtx: FileBeforeParseHook
const nuxtMock = {
callHook(hook: string, ctx: FileBeforeParseHook) {
if (hook === 'content:file:beforeParse') {
ctx.file.body = ctx.file.body.replace('replace-me', 'bar')
hookCtx = ctx
}
},
}
const content = await parseContent('content/index.md', `---
foo: 'replace-me'
---

# Hello World
`, collection, nuxtMock)
expect(hookCtx.file.id).toEqual('content/index.md')
expect(content.foo).toEqual('bar')
})
it('content:file:afterParse', async () => {
let hookCtx: FileAfterParseHook
const nuxtMock = {
callHook(hook: string, ctx: FileAfterParseHook) {
if (hook === 'content:file:afterParse') {
// augment
ctx.content.bar = 'foo'
hookCtx = ctx
}
},
}
const parsed = await parseContent('content/index.md', `---
foo: 'bar'
---

# Hello World
`, collection, nuxtMock)
expect(hookCtx.collection.name).toEqual('hookTest')
expect(parsed.id).toEqual('content/index.md')
expect(parsed.foo).toEqual('bar')
expect(parsed.bar).toEqual('foo')
})
})
Loading