diff --git a/playground/components/PropsExample.vue b/playground/components/PropsExample.vue new file mode 100644 index 000000000..d13b4b319 --- /dev/null +++ b/playground/components/PropsExample.vue @@ -0,0 +1,76 @@ + + + Props Example Component + + Count (number): {{ count }} + Title (string): {{ title }} + Is Active (boolean): {{ isActive ? 'Yes' : 'No' }} + + + + This content is shown when isActive is true! + Current count multiplied by 2: {{ count * 2 }} + + + + {{ title }} + + + + + + + diff --git a/playground/components/TestD.vue b/playground/components/TestD.vue new file mode 100644 index 000000000..63b2227a1 --- /dev/null +++ b/playground/components/TestD.vue @@ -0,0 +1,21 @@ + + + + + Test C + + diff --git a/playground/content.config.ts b/playground/content.config.ts index 44b32fc2a..396b14243 100644 --- a/playground/content.config.ts +++ b/playground/content.config.ts @@ -1,4 +1,4 @@ -import { defineContentConfig, defineCollectionSource, defineCollection, z } from '@nuxt/content' +import { defineContentConfig, defineCollectionSource, defineCollection, z, property } from '@nuxt/content' const hackernews = defineCollection({ type: 'data', @@ -34,6 +34,7 @@ const content = defineCollection({ schema: z.object({ date: z.date(), rawbody: z.string(), + testd: property(z.object({})).inherit('components/TestD.vue'), }), }) @@ -69,6 +70,11 @@ const collections = { content, data, pages, + buttons: defineCollection({ + type: 'data', + source: 'testd/**', + schema: property(z.object({})).inherit('@nuxt/ui/components/Button.vue'), + }), contentV2: defineCollection({ type: 'page', source: { diff --git a/src/types/schema.ts b/src/types/schema.ts index cbad8ae64..e6991f7e3 100644 --- a/src/types/schema.ts +++ b/src/types/schema.ts @@ -38,7 +38,7 @@ export interface Draft07DefinitionPropertyAllOf { export interface ContentConfig { editor?: EditorOptions // markdown?: boolean - // inherit?: string + inherit?: string } export interface EditorOptions { diff --git a/src/utils/collection.ts b/src/utils/collection.ts index 1ee0fbeef..f4d8834d3 100644 --- a/src/utils/collection.ts +++ b/src/utils/collection.ts @@ -3,7 +3,7 @@ import type { Collection, ResolvedCollection, CollectionSource, DefinedCollectio import { getOrderedSchemaKeys, describeProperty, getCollectionFieldsTypes } from '../runtime/internal/schema' import type { Draft07, ParsedContentFile } from '../types' import { defineLocalSource, defineGitHubSource, defineBitbucketSource } from './source' -import { emptyStandardSchema, mergeStandardSchema, metaStandardSchema, pageStandardSchema, infoStandardSchema, detectSchemaVendor } from './schema' +import { emptyStandardSchema, mergeStandardSchema, metaStandardSchema, pageStandardSchema, infoStandardSchema, detectSchemaVendor, replaceComponentSchemas } from './schema' import { logger } from './dev' import nuxtContentContext from './context' @@ -19,6 +19,7 @@ export function defineCollection(collection: Collection): DefinedCollectio const schemaCtx = nuxtContentContext().get(detectSchemaVendor(collection.schema)) standardSchema = schemaCtx.toJSONSchema(collection.schema!, '__SCHEMA__') } + standardSchema.definitions.__SCHEMA__ = replaceComponentSchemas(standardSchema.definitions.__SCHEMA__)! let extendedSchema: Draft07 = standardSchema if (collection.type === 'page') { diff --git a/src/utils/schema/index.ts b/src/utils/schema/index.ts index 34e6e0003..73fb4efa9 100644 --- a/src/utils/schema/index.ts +++ b/src/utils/schema/index.ts @@ -1,4 +1,7 @@ -import type { Draft07, EditorOptions, ContentConfig, ContentStandardSchemaV1 } from '../../types' +import type { Draft07, EditorOptions, ContentConfig, ContentStandardSchemaV1, Draft07Definition, Draft07DefinitionProperty } from '../../types' +import { resolveModule, useNuxt } from '@nuxt/kit' +import { getComponentMeta } from 'nuxt-component-meta/parser' +import { propsToJsonSchema } from 'nuxt-component-meta/utils' export * from './definitions' @@ -47,12 +50,12 @@ export function property(input: T): Property< // return receiver // } // } - // if (prop === 'inherit') { - // return (componentPath: string) => { - // $content.inherit = componentPath - // return receiver - // } - // } + if (prop === 'inherit') { + return (componentPath: string) => { + $content.inherit = componentPath + return receiver + } + } const value = Reflect.get(_target, prop, receiver) @@ -106,3 +109,34 @@ export function detectSchemaVendor(schema: ContentStandardSchemaV1) { return 'unknown' } + +export function replaceComponentSchemas(property: T): T { + if ((property as Draft07DefinitionProperty).type !== 'object') { + return property + } + // If the property has a `$content.inherit` property, replace it with the component's props schema + const $content = (property as Draft07DefinitionProperty).$content as ContentConfig + + if ($content?.inherit) { + const nuxt = useNuxt() + let path = String($content?.inherit) + try { + path = resolveModule(path) + } + catch { + // Ignore error + } + + const meta = getComponentMeta(path, { rootDir: nuxt.options.rootDir }) + return propsToJsonSchema(meta.props) as T + } + + // Look for `$content.inherit` properties in nested objects + if ((property as Draft07Definition).properties) { + Object.entries((property as Draft07Definition).properties).forEach(([key, value]) => { + (property as Draft07Definition).properties![key] = replaceComponentSchemas(value as Draft07DefinitionProperty) as Draft07DefinitionProperty + }) + } + + return property as T +} diff --git a/test/unit/replaceComponentSchemas.test.ts b/test/unit/replaceComponentSchemas.test.ts new file mode 100644 index 000000000..2ba16c944 --- /dev/null +++ b/test/unit/replaceComponentSchemas.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, vi } from 'vitest' + +import { replaceComponentSchemas } from '../../src/utils/schema' + +const mockedSchema = { + type: 'object', + properties: { + title: { type: 'string' }, + count: { type: 'number' }, + }, + required: ['title'], + additionalProperties: false, +} + +vi.mock('@nuxt/kit', () => ({ + useNuxt: () => ({ options: { rootDir: '/project/root' } }), + resolveModule: (p: string) => p.startsWith('~') ? `/resolved/${p.slice(1)}` : p, +})) + +vi.mock('nuxt-component-meta/parser', () => ({ + getComponentMeta: (path: string) => ({ + path, + props: [{ name: 'title' }, { name: 'count' }], + }), +})) + +vi.mock('nuxt-component-meta/utils', () => ({ + propsToJsonSchema: () => mockedSchema, +})) + +describe('replaceComponentSchemas', () => { + it('returns property unchanged when type is not object', () => { + const input = { type: 'string' } + const result = replaceComponentSchemas(input) + expect(result).toEqual(input) + }) + + it('replaces top-level object with $content.inherit using component props schema', () => { + const input = { + type: 'object', + $content: { inherit: '~/components/MyComponent.vue' }, + } + + const result = replaceComponentSchemas(input) + expect(result).toEqual(mockedSchema) + }) + + it('recursively replaces nested properties with $content.inherit and preserves others', () => { + const input = { + type: 'object', + properties: { + staticField: { type: 'string' }, + nested: { + type: 'object', + $content: { inherit: '~/components/Nested.vue' }, + }, + }, + required: [], + additionalProperties: false, + } + + const result = replaceComponentSchemas(input) + expect(result).toEqual({ + type: 'object', + properties: { + staticField: { type: 'string' }, + nested: mockedSchema, + }, + required: [], + additionalProperties: false, + }) + }) +})
Count (number): {{ count }}
Title (string): {{ title }}
Is Active (boolean): {{ isActive ? 'Yes' : 'No' }}
This content is shown when isActive is true!
Current count multiplied by 2: {{ count * 2 }}