Skip to content

Commit f17413d

Browse files
feat!: use vue-component-meta (#34)
* feat: use vue-component-meta * chore: remove logs * chore: set default options and cleanup * chore: add /api/component-meta tests * fix: update vue-component-meta, remove tsconfig requirement
1 parent 6f4f4cd commit f17413d

19 files changed

+2005
-2471
lines changed

package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,17 @@
2626
},
2727
"dependencies": {
2828
"@nuxt/kit": "^3.0.0-rc.3",
29-
"@vue/compiler-sfc": "^3.2.33",
30-
"pathe": "^0.3.3",
31-
"scule": "^0.2.1"
29+
"scule": "^0.3.2",
30+
"vue-component-meta": "^0.40.2"
3231
},
3332
"devDependencies": {
3433
"@iconify/vue": "^3.2.1",
3534
"@nuxt/module-builder": "latest",
36-
"@nuxt/test-utils": "^3.0.0-rc.3",
35+
"@nuxt/test-utils": "^3.0.0-rc.8",
3736
"@nuxtjs/eslint-config-typescript": "latest",
3837
"eslint": "latest",
39-
"nuxt": "^3.0.0-rc.3",
38+
"nuxt": "^3.0.0-rc.8",
4039
"standard-version": "^9.3.2",
41-
"vitest": "^0.10.2"
40+
"vitest": "^0.22.1"
4241
}
4342
}

playground/app.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
<template>
22
<div>
3-
<h2>Components</h2>
3+
<h2>Components from <code>/api/component-meta</code> nitro route</h2>
44
<pre>{{ data }}</pre>
5+
<hr />
6+
<h2>Components from <code>#nuxt-component-meta</code> virtual module</h2>
7+
<pre>{{ components }}</pre>
58
</div>
69
</template>
710

811
<script setup>
12+
import components from '#nuxt-component-meta'
913
const { data } = await useAsyncData('metas', () => $fetch('/api/component-meta'))
1014
</script>

playground/components/TestComponent.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
77
</template>
88

99
<script setup>
10-
const props = defineProps({
10+
defineProps({
11+
foo: {
12+
type: String,
13+
required: true
14+
},
15+
/**
16+
* The hello property.
17+
*
18+
* @since v1.0.0
19+
*/
1120
hello: {
1221
type: String,
1322
default: 'Hello'
@@ -22,5 +31,4 @@ const props = defineProps({
2231
}
2332
})
2433
const emit = defineEmits(['change', 'delete'])
25-
2634
</script>

playground/components/testTyped.vue

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@
77
</template>
88

99
<script setup lang="ts">
10-
const props = defineProps<{
10+
withDefaults(defineProps<{
1111
hello: string,
1212
booleanProp?: boolean,
13-
numberProp?: number
14-
}>()
15-
const emit = defineEmits(['change', 'delete'])
16-
13+
numberProp?: number,
14+
/**
15+
* The foo array property.
16+
*
17+
* @since v1.0.0
18+
*/
19+
foo?: string[]
20+
}>(), {
21+
numberProp: 42,
22+
foo: () => ['bar', 'baz']
23+
})
24+
defineEmits(['change', 'delete'])
1725
</script>

playground/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./.nuxt/tsconfig.json"
3+
}

src/module.ts

Lines changed: 145 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import { readFile } from 'fs/promises'
2-
import { basename } from 'pathe'
3-
import { defineNuxtModule, resolveModule, createResolver, addServerHandler } from '@nuxt/kit'
4-
import { parseComponent } from './utils/parseComponent'
5-
import type { ComponentProp, ComponentSlot, HookData } from './types'
1+
import type { MetaCheckerOptions } from 'vue-component-meta'
2+
import {
3+
addServerHandler,
4+
addTemplate,
5+
createResolver,
6+
defineNuxtModule,
7+
resolveModule
8+
} from '@nuxt/kit'
69

7-
export interface ModuleOptions {}
10+
import { createComponentMetaCheckerByJsonConfig } from 'vue-component-meta'
11+
import type { HookData } from './types'
812

13+
export interface ModuleOptions {
14+
checkerOptions?: MetaCheckerOptions
15+
}
916
export interface ModuleHooks {
1017
'component-meta:parsed'(data: HookData): void
1118
}
@@ -15,44 +22,153 @@ export default defineNuxtModule<ModuleOptions>({
1522
name: 'nuxt-component-meta',
1623
configKey: 'componentMeta'
1724
},
18-
setup (_options, nuxt) {
25+
defaults: () => ({
26+
checkerOptions: {
27+
forceUseTs: true,
28+
schema: {}
29+
}
30+
}),
31+
setup (options, nuxt) {
1932
const resolver = createResolver(import.meta.url)
2033

21-
let componentMeta
34+
let componentMeta: any = {}
35+
36+
// default to empty permisive object if no componentMeta is defined
37+
const script = ['export const components = {}', 'export default components']
38+
const dts = [
39+
"import type { NuxtComponentMeta } from 'nuxt-component-meta'",
40+
'export type { NuxtComponentMeta }',
41+
'export type NuxtComponentMetaNames = string',
42+
'declare const components: Record<NuxtComponentMetaNames, NuxtComponentMeta>',
43+
'export { components as default, components }'
44+
]
45+
2246
nuxt.hook('components:extend', async (components) => {
23-
componentMeta = await Promise.all(
24-
components.map(async (component) => {
25-
const path = resolveModule((component as any).filePath, { paths: nuxt.options.rootDir })
26-
const source = await readFile(path, { encoding: 'utf-8' })
27-
28-
const data: HookData = {
29-
meta: {
30-
name: (component as any).pascalName,
31-
global: Boolean(component.global),
32-
props: [] as ComponentProp[],
33-
slots: [] as ComponentSlot[]
34-
},
35-
path,
36-
source
37-
}
38-
39-
const { props, slots } = parseComponent(data.meta.name, source, { filename: basename(path) })
40-
data.meta.props = props
47+
const checker = createComponentMetaCheckerByJsonConfig(
48+
nuxt.options.rootDir,
49+
{
50+
extends: '../tsconfig.json',
51+
include: [
52+
'**/*'
53+
]
54+
},
55+
options.checkerOptions
56+
)
57+
58+
function reducer (acc: any, component: any) {
59+
if (component.name) {
60+
acc[component.name] = component
61+
}
62+
63+
return acc
64+
}
65+
66+
async function mapper (component: any): Promise<HookData['meta']> {
67+
const path = resolveModule(component.filePath, {
68+
paths: nuxt.options.rootDir
69+
})
70+
71+
const data = {
72+
meta: {
73+
name: component.pascalName,
74+
global: Boolean(component.global),
75+
props: [],
76+
slots: [],
77+
events: [],
78+
exposed: []
79+
},
80+
path,
81+
source: ''
82+
} as HookData
83+
84+
if (!checker) {
85+
return data.meta
86+
}
87+
88+
try {
89+
const { props, slots, events, exposed } = checker?.getComponentMeta(path)
90+
4191
data.meta.slots = slots
92+
data.meta.events = events
93+
data.meta.exposed = exposed
94+
data.meta.props = props
95+
.filter(prop => !prop.global)
96+
.sort((a, b) => {
97+
// sort required properties first
98+
if (!a.required && b.required) {
99+
return 1
100+
}
101+
if (a.required && !b.required) {
102+
return -1
103+
}
104+
// then ensure boolean properties are sorted last
105+
if (a.type === 'boolean' && b.type !== 'boolean') {
106+
return 1
107+
}
108+
if (a.type !== 'boolean' && b.type === 'boolean') {
109+
return -1
110+
}
111+
112+
return 0
113+
})
42114

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

46-
return data.meta
47-
})
121+
return data.meta
122+
}
123+
124+
componentMeta = (await Promise.all(components.map(mapper))).reduce(
125+
reducer,
126+
{}
48127
)
128+
129+
// generate virtual script
130+
script.splice(0, script.length)
131+
script.push(`export const components = ${JSON.stringify(componentMeta)}`)
132+
script.push('export default components')
133+
134+
for (const key in componentMeta) {
135+
script.push(`export const meta${key} = ${JSON.stringify(
136+
componentMeta[key]
137+
)}`)
138+
}
139+
140+
// generate typescript definition file
141+
const componentMetaKeys = Object.keys(componentMeta)
142+
const componentNameString = componentMetaKeys.map(name => `"${name}"`)
143+
const exportNames = componentMetaKeys.map(name => `meta${name}`)
144+
145+
dts.splice(2, script.length) // keep the two first lines (import type and export NuxtComponentMeta)
146+
dts.push(`export type NuxtComponentMetaNames = ${componentNameString.join(' | ')}`)
147+
dts.push('declare const components: Record<NuxtComponentMetaNames, NuxtComponentMeta>')
148+
149+
for (const exportName of exportNames) {
150+
dts.push(`declare const ${exportName}: NuxtComponentMeta`)
151+
}
152+
153+
dts.push(`export { components as default, components, ${exportNames.join(', ')} }`)
154+
})
155+
156+
const template = addTemplate({
157+
filename: 'nuxt-component-meta.mjs',
158+
getContents: () => script.join('\n')
159+
})
160+
addTemplate({
161+
filename: 'nuxt-component-meta.d.ts',
162+
getContents: () => dts.join('\n'),
163+
write: true
49164
})
165+
nuxt.options.alias['#nuxt-component-meta'] = template.dst!
50166

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

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

58174
addServerHandler({

src/runtime/server/api/component-meta.get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineEventHandler((event) => {
99
const componentName = event.context.params['component?']
1010

1111
if (componentName) {
12-
const meta = components.find(c => c.name === pascalCase(componentName))
12+
const meta = components[pascalCase(componentName)]
1313
if (!meta) {
1414
throw createError({
1515
statusMessage: 'Components not found!',

src/types.d.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,9 @@
1-
export interface ComponentPropType {
2-
type: string | string[]
3-
elementType?: string
4-
as?: string | ComponentPropType
5-
}
6-
7-
export interface ComponentProp {
8-
name: string
9-
type?: string | ComponentPropType | string[],
10-
default?: any
11-
required?: boolean,
12-
values?: any,
13-
description?: string
14-
}
1+
import type { ComponentMeta } from 'vue-component-meta'
152

16-
export interface ComponentSlot {
17-
name: string
18-
}
3+
export type NuxtComponentMeta = ComponentMeta & { name: string, global?: boolean }
194

205
export interface HookData {
21-
meta: {
22-
name: string
23-
global: boolean
24-
props: ComponentProp[]
25-
slots: ComponentSlot[]
26-
}
6+
meta: NuxtComponentMeta
277
path: string
288
source: string
299
}

0 commit comments

Comments
 (0)