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
76 changes: 76 additions & 0 deletions playground/components/PropsExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<div class="props-example">
<h3>Props Example Component</h3>
<div class="props-display">
<p><strong>Count (number):</strong> {{ count }}</p>
<p><strong>Title (string):</strong> {{ title }}</p>
<p><strong>Is Active (boolean):</strong> {{ isActive ? 'Yes' : 'No' }}</p>
</div>

<div
v-if="isActive"
class="conditional-content"
>
<p>This content is shown when isActive is true!</p>
<p>Current count multiplied by 2: {{ count * 2 }}</p>
</div>

<div
class="styled-title"
:class="{ active: isActive }"
>
{{ title }}
</div>
</div>
</template>

<script setup lang="ts">
interface Props {
count: number
title: string
isActive: boolean
}

efineProps<Props>()

// You can also define props with default values like this:
// const props = withDefaults(defineProps<Props>(), {
// count: 0,
// title: 'Default Title',
// isActive: false
// })
</script>

<style scoped>
.props-example {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
margin: 10px 0;
}

.props-display {
background-color: #f5f5f5;
padding: 15px;
border-radius: 4px;
margin: 10px 0;
}

.conditional-content {
background-color: #e6f3ff;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}

.styled-title {
font-size: 1.5em;
font-weight: bold;
color: #666;
transition: color 0.3s ease;
}

.styled-title.active {
color: #007bff;
}
</style>
21 changes: 21 additions & 0 deletions playground/components/TestD.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts" setup>
defineProps<{
msg: string
count: number
disabled: boolean
/**
* FOOOOOO
*/
foo: string[]
/**
* BARRRRR
*/
bar?: number
}>()
</script>

<template>
<span class="text-green-500">
Test C <slot />
</span>
</template>
8 changes: 7 additions & 1 deletion playground/content.config.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -34,6 +34,7 @@ const content = defineCollection({
schema: z.object({
date: z.date(),
rawbody: z.string(),
testd: property(z.object({})).inherit('components/TestD.vue'),
}),
})

Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface Draft07DefinitionPropertyAllOf {
export interface ContentConfig {
editor?: EditorOptions
// markdown?: boolean
// inherit?: string
inherit?: string
}

export interface EditorOptions {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -19,6 +19,7 @@ export function defineCollection<T>(collection: Collection<T>): 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') {
Expand Down
48 changes: 41 additions & 7 deletions src/utils/schema/index.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -47,12 +50,12 @@ export function property<T extends ContentStandardSchemaV1>(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)

Expand Down Expand Up @@ -106,3 +109,34 @@ export function detectSchemaVendor(schema: ContentStandardSchemaV1) {

return 'unknown'
}

export function replaceComponentSchemas<T = Draft07Definition | Draft07DefinitionProperty>(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
}
73 changes: 73 additions & 0 deletions test/unit/replaceComponentSchemas.test.ts
Original file line number Diff line number Diff line change
@@ -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,
})
})
})
Loading