diff --git a/docs/content/docs/8.advanced/5.hooks.md b/docs/content/docs/8.advanced/5.hooks.md index 5c8d48769..363c6d272 100644 --- a/docs/content/docs/8.advanced/5.hooks.md +++ b/docs/content/docs/8.advanced/5.hooks.md @@ -36,6 +36,21 @@ export default defineNuxtConfig({ }) ``` +## `content:manifest`{lang="ts"} + +This hook is called after the manifest is generated. + +```ts +export default defineNuxtConfig({ + hooks: { + 'content:manifest'(ctx) { + // ... + // ctx.collections.push(resolveCollection('name', defineCollection())) + } + } +}) +``` + ## Example Usage ```ts [nuxt.config.ts] diff --git a/src/module.ts b/src/module.ts index 99603aae6..0843832d4 100644 --- a/src/module.ts +++ b/src/module.ts @@ -22,7 +22,13 @@ import { kebabCase, pascalCase } from 'scule' import defu from 'defu' import { version } from '../package.json' import { generateCollectionInsert, generateCollectionTableDefinition } from './utils/collection' -import { componentsManifestTemplate, contentTypesTemplate, fullDatabaseRawDumpTemplate, manifestTemplate, moduleTemplates } from './utils/templates' +import { + componentsManifestTemplate, + contentTypesTemplate, + fullDatabaseRawDumpTemplate, + manifestTemplate, + moduleTemplates, +} from './utils/templates' import type { ResolvedCollection } from './types/collection' import type { ModuleOptions } from './types/module' import { getContentChecksum, logger, chunks, NuxtContentHMRUnplugin } from './utils/dev' @@ -120,6 +126,7 @@ export default defineNuxtModule({ const { collections } = await loadContentConfig(nuxt, options) manifest.collections = collections + await nuxt.callHook('content:manifest', manifest) nuxt.options.vite.optimizeDeps = defu(nuxt.options.vite.optimizeDeps, { exclude: ['@sqlite.org/sqlite-wasm'], @@ -191,7 +198,10 @@ export default defineNuxtModule({ const preset = findPreset(nuxt) await preset.setupNitro(config, { manifest, resolver, moduleOptions: options, nuxt }) - const resolveOptions = { resolver, sqliteConnector: options.experimental?.sqliteConnector || (options.experimental?.nativeSqlite ? 'native' : undefined) } + const resolveOptions = { + resolver, + sqliteConnector: options.experimental?.sqliteConnector || (options.experimental?.nativeSqlite ? 'native' : undefined), + } config.alias ||= {} config.alias['#content/adapter'] = await resolveDatabaseAdapter(config.runtimeConfig!.content!.database?.type || options.database.type, resolveOptions) config.alias['#content/local-adapter'] = await resolveDatabaseAdapter(options._localDatabase!.type || 'sqlite', resolveOptions) @@ -400,13 +410,20 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio // NOTE: all queries having the structure comment at the end, will be ignored at init if no // structure changes are detected in the structureVersion `${generateCollectionTableDefinition(infoCollection, { drop: false })} -- structure`, - ...generateCollectionInsert(infoCollection, { id: `checksum_${collection.name}`, version, structureVersion, ready: false }).queries.map(row => `${row} -- meta`), + ...generateCollectionInsert(infoCollection, { + id: `checksum_${collection.name}`, + version, + structureVersion, + ready: false, + }).queries.map(row => `${row} -- meta`), // Insert queries for the collection ...collectionQueries, // and finally when we are finished, we update the info table to say that the init is done - `UPDATE ${infoCollection.tableName} SET ready = true WHERE id = 'checksum_${collection.name}'; -- meta`, + `UPDATE ${infoCollection.tableName} + SET ready = true + WHERE id = 'checksum_${collection.name}'; -- meta`, ] } @@ -460,6 +477,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio } const proseTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'strong', 'em', 's', 'code', 'span', 'blockquote', 'pre', 'hr', 'img', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'th', 'td'] + function getMappedTag(tag: string, additionalTags: Record = {}) { if (proseTags.includes(tag)) { return `prose-${tag}` diff --git a/src/types/hooks.ts b/src/types/hooks.ts index 331b22035..69004a1ef 100644 --- a/src/types/hooks.ts +++ b/src/types/hooks.ts @@ -1,6 +1,7 @@ import type { ResolvedCollection } from './collection' import type { ContentFile, ParsedContentFile } from './content' import type { PathMetaOptions } from './path-meta' +import type { Manifest } from './manifest' // Parser options interface interface ParserOptions { @@ -32,5 +33,6 @@ declare module '@nuxt/schema' { interface NuxtHooks { 'content:file:beforeParse': (ctx: FileBeforeParseHook) => Promise | void 'content:file:afterParse': (ctx: FileAfterParseHook) => Promise | void + 'content:manifest': (manifest: Manifest) => Promise | void } } diff --git a/src/utils/index.ts b/src/utils/index.ts index a4088891e..992ffd218 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ export { metaStandardSchema, pageStandardSchema, property } from './schema' -export { defineCollection, defineCollectionSource } from './collection' +export { resolveCollection, defineCollection, defineCollectionSource } from './collection' export { defineContentConfig } from './config' export { defineTransformer } from './content/transformers/utils' diff --git a/test/unit/hooks.test.ts b/test/unit/hooks.test.ts index 6b02335e3..1a1ac07a9 100644 --- a/test/unit/hooks.test.ts +++ b/test/unit/hooks.test.ts @@ -5,6 +5,7 @@ import { resolveCollection } from '../../src/utils/collection' import { parseContent } from '../utils/content' import type { FileAfterParseHook, FileBeforeParseHook } from '../../src/types' import { initiateValidatorsContext } from '../../src/utils/dependencies' +import type { Manifest } from '../../src/types/manifest' describe('Hooks', async () => { await initiateValidatorsContext() @@ -57,4 +58,40 @@ foo: 'bar' expect(parsed.foo).toEqual('bar') expect(parsed.bar).toEqual('foo') }) + + it('content:manifest mutations are reflected in manifest', async () => { + const extraCollection = resolveCollection('injected', defineCollection({ + type: 'data', + source: 'extra/**', + schema: z.object({ + body: z.any(), + }), + }))! + + const manifest: Manifest = { + checksumStructure: {}, + checksum: {}, + dump: {}, + components: [], + collections: [collection], + } + + // Simulate the module calling the hook + const nuxtMock = { + callHook(hook: string, ctx: Manifest) { + if (hook === 'content:manifest') { + ctx.collections.push(extraCollection) + } + }, + } + + nuxtMock.callHook('content:manifest', manifest) + + // must be visible on original manifest object + expect(manifest.collections).toHaveLength(2) + // new collection is visible + expect(manifest.collections.find(c => c.name === 'injected')).toBeDefined() + // original collection still exists + expect(manifest.collections.find(c => c.name === 'hookTest')).toBeDefined() + }) })