Skip to content

feat: inherit component prop types in content collection #3451

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"micromatch": "^4.0.8",
"minimark": "^0.2.0",
"minimatch": "^10.0.3",
"nuxt-component-meta": "^0.12.1",
"nuxt-component-meta": "https://pkg.pr.new/nuxt-component-meta@ff0edad",
"nypm": "^0.6.0",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
Expand Down
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>
6 changes: 6 additions & 0 deletions playground/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const content = defineCollection({
schema: z.object({
date: z.date(),
rawbody: z.string(),
testd: z.component('components/TestD.vue'),
}),
})

Expand Down Expand Up @@ -69,6 +70,11 @@ const collections = {
content,
data,
pages,
buttons: defineCollection({
type: 'data',
source: 'testd/**',
schema: z.component('@nuxt/ui/components/Button.vue'),
}),
contentV2: defineCollection({
type: 'page',
source: {
Expand Down
18 changes: 13 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/utils/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,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 } from './schema'
import { emptyStandardSchema, mergeStandardSchema, metaStandardSchema, pageStandardSchema, replaceComponentSchemas } from './schema'
import { z, zodToStandardSchema } from './zod'
import { logger } from './dev'

Expand All @@ -17,6 +17,7 @@ export function defineCollection<T extends ZodRawShape>(collection: Collection<T
if (collection.schema instanceof z.ZodObject) {
standardSchema = zodToStandardSchema(collection.schema, '__SCHEMA__')
}
standardSchema.definitions.__SCHEMA__ = replaceComponentSchemas(standardSchema.definitions.__SCHEMA__)

let extendedSchema: Draft07 = standardSchema
if (collection.type === 'page') {
Expand Down
35 changes: 34 additions & 1 deletion src/utils/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as z from 'zod'
import { ContentFileExtension } from '../types/content'
import type { Draft07 } from '../types'
import type { Draft07, 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 function getEnumValues<T extends Record<string, unknown>>(obj: T) {
return Object.values(obj) as [(typeof obj)[keyof T]]
Expand Down Expand Up @@ -204,3 +207,33 @@ export function mergeStandardSchema(s1: Draft07, s2: Draft07): Draft07 {
),
}
}

export function replaceComponentSchemas<T extends Draft07Definition | Draft07DefinitionProperty>(property: T): T {
if (property.type !== 'object') {
return property
}

// If the property has a _inherit property, replace it with the component's props schema
if (property.properties?._inherit) {
const nuxt = useNuxt()
let path = String((property.properties._inherit as Draft07DefinitionProperty).default)
try {
path = resolveModule(path)
}
catch {
// Ignore error
}

const meta = getComponentMeta(path, { rootDir: nuxt.options.rootDir })
return propsToJsonSchema(meta.props) as T
}

// Look for _inherit properties in nested objects
if (property.properties) {
Object.entries(property.properties).forEach(([key, value]) => {
property.properties![key] = replaceComponentSchemas(value as Draft07DefinitionProperty) as Draft07DefinitionProperty
})
}

return property
}
9 changes: 8 additions & 1 deletion src/utils/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export type SqlFieldType = 'VARCHAR' | 'INT' | 'BOOLEAN' | 'DATE' | 'TEXT'
return this
}

export const z = zod
export const z = {
...zod,
component(component: string) {
return zod.object({
_inherit: zod.string().default(component),
})
},
}

// Function to get the underlying Zod type
export function getUnderlyingType(zodType: ZodType): ZodType {
Expand Down