Skip to content

Commit

Permalink
feat: pre / post parsing hooks (#2925)
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw authored Dec 24, 2024
1 parent 4cdf757 commit c2c98d8
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 6 deletions.
41 changes: 41 additions & 0 deletions docs/content/docs/7.advanced/5.hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
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
import type { FileBeforeParseHook } from '@nuxt/content'

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

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

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

```ts
import type { FileAfterParseHook } from '@nuxt/content'

export default defineNuxtConfig({
hooks: {
'content:file:afterParse'(ctx: FileAfterParseHook) {
// ...
}
}
})
```
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: Readonly<ResolvedCollection>
file: ContentFile
parserOptions: TransformContentOptions
}
export interface FileAfterParseHook {
collection: Readonly<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
17 changes: 13 additions & 4 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,8 +133,12 @@ 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 } = 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>
Expand Down Expand Up @@ -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')
})
})

0 comments on commit c2c98d8

Please sign in to comment.