Skip to content

feat!: use vue-component-meta #34

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 6 commits into from
Aug 29, 2022
Merged
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
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -26,18 +26,17 @@
},
"dependencies": {
"@nuxt/kit": "^3.0.0-rc.3",
"@vue/compiler-sfc": "^3.2.33",
"pathe": "^0.3.3",
"scule": "^0.2.1"
"scule": "^0.3.2",
"vue-component-meta": "^0.40.2"
},
"devDependencies": {
"@iconify/vue": "^3.2.1",
"@nuxt/module-builder": "latest",
"@nuxt/test-utils": "^3.0.0-rc.3",
"@nuxt/test-utils": "^3.0.0-rc.8",
"@nuxtjs/eslint-config-typescript": "latest",
"eslint": "latest",
"nuxt": "^3.0.0-rc.3",
"nuxt": "^3.0.0-rc.8",
"standard-version": "^9.3.2",
"vitest": "^0.10.2"
"vitest": "^0.22.1"
}
}
6 changes: 5 additions & 1 deletion playground/app.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<template>
<div>
<h2>Components</h2>
<h2>Components from <code>/api/component-meta</code> nitro route</h2>
<pre>{{ data }}</pre>
<hr />
<h2>Components from <code>#nuxt-component-meta</code> virtual module</h2>
<pre>{{ components }}</pre>
</div>
</template>

<script setup>
import components from '#nuxt-component-meta'
const { data } = await useAsyncData('metas', () => $fetch('/api/component-meta'))
</script>
12 changes: 10 additions & 2 deletions playground/components/TestComponent.vue
Original file line number Diff line number Diff line change
@@ -7,7 +7,16 @@
</template>

<script setup>
const props = defineProps({
defineProps({
foo: {
type: String,
required: true
},
/**
* The hello property.
*
* @since v1.0.0
*/
hello: {
type: String,
default: 'Hello'
@@ -22,5 +31,4 @@ const props = defineProps({
}
})
const emit = defineEmits(['change', 'delete'])

</script>
18 changes: 13 additions & 5 deletions playground/components/testTyped.vue
Original file line number Diff line number Diff line change
@@ -7,11 +7,19 @@
</template>

<script setup lang="ts">
const props = defineProps<{
withDefaults(defineProps<{
hello: string,
booleanProp?: boolean,
numberProp?: number
}>()
const emit = defineEmits(['change', 'delete'])

numberProp?: number,
/**
* The foo array property.
*
* @since v1.0.0
*/
foo?: string[]
}>(), {
numberProp: 42,
foo: () => ['bar', 'baz']
})
defineEmits(['change', 'delete'])
</script>
3 changes: 3 additions & 0 deletions playground/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}
174 changes: 145 additions & 29 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { readFile } from 'fs/promises'
import { basename } from 'pathe'
import { defineNuxtModule, resolveModule, createResolver, addServerHandler } from '@nuxt/kit'
import { parseComponent } from './utils/parseComponent'
import type { ComponentProp, ComponentSlot, HookData } from './types'
import type { MetaCheckerOptions } from 'vue-component-meta'
import {
addServerHandler,
addTemplate,
createResolver,
defineNuxtModule,
resolveModule
} from '@nuxt/kit'

export interface ModuleOptions {}
import { createComponentMetaCheckerByJsonConfig } from 'vue-component-meta'
import type { HookData } from './types'

export interface ModuleOptions {
checkerOptions?: MetaCheckerOptions
}
export interface ModuleHooks {
'component-meta:parsed'(data: HookData): void
}
@@ -15,44 +22,153 @@ export default defineNuxtModule<ModuleOptions>({
name: 'nuxt-component-meta',
configKey: 'componentMeta'
},
setup (_options, nuxt) {
defaults: () => ({
checkerOptions: {
forceUseTs: true,
schema: {}
}
}),
setup (options, nuxt) {
const resolver = createResolver(import.meta.url)

let componentMeta
let componentMeta: any = {}

// default to empty permisive object if no componentMeta is defined
const script = ['export const components = {}', 'export default components']
const dts = [
"import type { NuxtComponentMeta } from 'nuxt-component-meta'",
'export type { NuxtComponentMeta }',
'export type NuxtComponentMetaNames = string',
'declare const components: Record<NuxtComponentMetaNames, NuxtComponentMeta>',
'export { components as default, components }'
]

nuxt.hook('components:extend', async (components) => {
componentMeta = await Promise.all(
components.map(async (component) => {
const path = resolveModule((component as any).filePath, { paths: nuxt.options.rootDir })
const source = await readFile(path, { encoding: 'utf-8' })

const data: HookData = {
meta: {
name: (component as any).pascalName,
global: Boolean(component.global),
props: [] as ComponentProp[],
slots: [] as ComponentSlot[]
},
path,
source
}

const { props, slots } = parseComponent(data.meta.name, source, { filename: basename(path) })
data.meta.props = props
const checker = createComponentMetaCheckerByJsonConfig(
nuxt.options.rootDir,
{
extends: '../tsconfig.json',
include: [
'**/*'
]
},
options.checkerOptions
)

function reducer (acc: any, component: any) {
if (component.name) {
acc[component.name] = component
}

return acc
}

async function mapper (component: any): Promise<HookData['meta']> {
const path = resolveModule(component.filePath, {
paths: nuxt.options.rootDir
})

const data = {
meta: {
name: component.pascalName,
global: Boolean(component.global),
props: [],
slots: [],
events: [],
exposed: []
},
path,
source: ''
} as HookData

if (!checker) {
return data.meta
}

try {
const { props, slots, events, exposed } = checker?.getComponentMeta(path)

data.meta.slots = slots
data.meta.events = events
data.meta.exposed = exposed
data.meta.props = props
.filter(prop => !prop.global)
.sort((a, b) => {
// sort required properties first
if (!a.required && b.required) {
return 1
}
if (a.required && !b.required) {
return -1
}
// then ensure boolean properties are sorted last
if (a.type === 'boolean' && b.type !== 'boolean') {
return 1
}
if (a.type !== 'boolean' && b.type === 'boolean') {
return -1
}

return 0
})

// @ts-ignore
await nuxt.callHook('component-meta:parsed', data)
} catch (error: any) {
console.error(`Unable to parse component "${path}": ${error}`)
}

return data.meta
})
return data.meta
}

componentMeta = (await Promise.all(components.map(mapper))).reduce(
reducer,
{}
)

// generate virtual script
script.splice(0, script.length)
script.push(`export const components = ${JSON.stringify(componentMeta)}`)
script.push('export default components')

for (const key in componentMeta) {
script.push(`export const meta${key} = ${JSON.stringify(
componentMeta[key]
)}`)
}

// generate typescript definition file
const componentMetaKeys = Object.keys(componentMeta)
const componentNameString = componentMetaKeys.map(name => `"${name}"`)
const exportNames = componentMetaKeys.map(name => `meta${name}`)

dts.splice(2, script.length) // keep the two first lines (import type and export NuxtComponentMeta)
dts.push(`export type NuxtComponentMetaNames = ${componentNameString.join(' | ')}`)
dts.push('declare const components: Record<NuxtComponentMetaNames, NuxtComponentMeta>')

for (const exportName of exportNames) {
dts.push(`declare const ${exportName}: NuxtComponentMeta`)
}

dts.push(`export { components as default, components, ${exportNames.join(', ')} }`)
})

const template = addTemplate({
filename: 'nuxt-component-meta.mjs',
getContents: () => script.join('\n')
})
addTemplate({
filename: 'nuxt-component-meta.d.ts',
getContents: () => dts.join('\n'),
write: true
})
nuxt.options.alias['#nuxt-component-meta'] = template.dst!

nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.handlers = nitroConfig.handlers || []
nitroConfig.virtual = nitroConfig.virtual || {}

nitroConfig.virtual['#meta/virtual/meta'] = () => `export const components = ${JSON.stringify(componentMeta)}`
nitroConfig.virtual['#meta/virtual/meta'] = () => script.join('\n')
})

addServerHandler({
2 changes: 1 addition & 1 deletion src/runtime/server/api/component-meta.get.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ export default defineEventHandler((event) => {
const componentName = event.context.params['component?']

if (componentName) {
const meta = components.find(c => c.name === pascalCase(componentName))
const meta = components[pascalCase(componentName)]
if (!meta) {
throw createError({
statusMessage: 'Components not found!',
26 changes: 3 additions & 23 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
export interface ComponentPropType {
type: string | string[]
elementType?: string
as?: string | ComponentPropType
}

export interface ComponentProp {
name: string
type?: string | ComponentPropType | string[],
default?: any
required?: boolean,
values?: any,
description?: string
}
import type { ComponentMeta } from 'vue-component-meta'

export interface ComponentSlot {
name: string
}
export type NuxtComponentMeta = ComponentMeta & { name: string, global?: boolean }

export interface HookData {
meta: {
name: string
global: boolean
props: ComponentProp[]
slots: ComponentSlot[]
}
meta: NuxtComponentMeta
path: string
source: string
}
Loading