@poveste/plugin-vue 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/bundled/client/app/MountStory.js +32 -30
- package/dist/bundled/client/app/RenderStory.js +39 -44
- package/dist/bundled/client/app/Story.js +62 -60
- package/dist/bundled/client/app/global-components.js +5 -3
- package/dist/bundled/client/app/host.js +9 -5
- package/dist/bundled/client/app/util.js +46 -33
- package/dist/bundled/client/server/Story.js +13 -0
- package/dist/bundled/client/server/collect.js +41 -0
- package/dist/bundled/client/server/run.js +10 -39
- package/dist/bundled/client/server.js +1 -2
- package/dist/bundled/testing/index.js +168 -0
- package/dist/client/app/Story.d.ts +19 -1
- package/dist/client/app/Variant.d.ts +2 -2
- package/dist/client/app/global-components.d.ts +1 -0
- package/dist/client/app/host.d.ts +8 -0
- package/dist/client/app/util.d.ts +28 -0
- package/dist/client/server/Story.d.ts +21 -3
- package/dist/client/server/collect.d.ts +14 -0
- package/dist/client-dependencies.d.ts +2 -0
- package/dist/client-dependencies.js +2 -0
- package/dist/docgen/events.d.ts +9 -0
- package/dist/docgen/events.js +45 -0
- package/dist/docgen/extractor.d.ts +7 -0
- package/dist/docgen/extractor.js +60 -0
- package/dist/docgen/generic.d.ts +5 -0
- package/dist/docgen/generic.js +19 -0
- package/dist/docgen/meta.d.ts +12 -0
- package/dist/docgen/meta.js +76 -0
- package/dist/docgen/slots.d.ts +6 -0
- package/dist/docgen/slots.js +21 -0
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +24 -11
- package/dist/testing/index.d.ts +28 -0
- package/dist/testing/vite.d.ts +18 -0
- package/dist/testing/vite.js +171 -0
- package/dist/util/project-vue.d.ts +1 -0
- package/dist/util/project-vue.js +12 -0
- package/dist/util/vue-plugin.d.ts +10 -0
- package/dist/util/vue-plugin.js +28 -0
- package/package.json +33 -15
- package/src/client/app/MountStory.ts +2 -0
- package/src/client/app/RenderStory.ts +2 -1
- package/src/client/app/Story.ts +15 -3
- package/src/client/app/global-components.ts +4 -0
- package/src/client/app/host.ts +21 -2
- package/src/client/app/util.ts +65 -0
- package/src/client/server/Story.ts +15 -2
- package/src/client/server/collect.ts +83 -0
- package/src/client/server/run.ts +3 -63
- package/src/client-dependencies.ts +2 -0
- package/src/docgen/events.ts +55 -0
- package/src/docgen/extractor.ts +67 -0
- package/src/docgen/generic.ts +20 -0
- package/src/docgen/meta.ts +93 -0
- package/src/docgen/slots.ts +33 -0
- package/src/index.node.ts +27 -12
- package/src/testing/index.ts +286 -0
- package/src/testing/vite.ts +194 -0
- package/src/util/project-vue.ts +13 -0
- package/src/util/vue-plugin.ts +32 -0
- package/testing.d.ts +2 -0
package/src/client/app/util.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentInternalInstance } from 'vue'
|
|
2
2
|
import { applyState, createStateBaseline, isMarkedRaw, isPlainObject } from '@poveste/shared'
|
|
3
3
|
import {
|
|
4
|
+
getCurrentInstance as _getCurrentInstance,
|
|
4
5
|
isRef as _isRef,
|
|
5
6
|
unref as _unref,
|
|
6
7
|
watch as _watch,
|
|
@@ -234,6 +235,46 @@ export function isWritableBinding(value: unknown): boolean {
|
|
|
234
235
|
return !(isRef(value) && isReadonly(value))
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
/**
|
|
239
|
+
* A component a story holds in its setup, which implicit state skips.
|
|
240
|
+
*
|
|
241
|
+
* Each key is asked with `in` before it is read. Nuxt's runtime config is, in
|
|
242
|
+
* dev, a proxy that warns on any key it does not carry, and reading `__file`,
|
|
243
|
+
* `render` and `setup` off it printed `NUXT_E1003` three times a mount (#1454).
|
|
244
|
+
*/
|
|
245
|
+
export function isComponentBinding(value: unknown): boolean {
|
|
246
|
+
if (typeof value === 'function') {
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
if (!isObject(value)) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
return ('__file' in value && !!value.__file)
|
|
253
|
+
|| ('render' in value && typeof value.render === 'function')
|
|
254
|
+
|| ('setup' in value && typeof value.setup === 'function')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isVueApp(value: unknown): boolean {
|
|
258
|
+
return isObject(value) && '_context' in value && '_component' in value && 'mount' in value
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A Vue app, or an object holding one in a data property of its own, as the
|
|
263
|
+
* Nuxt app `useNuxtApp()` returns does. Implicit state skips it: the state sync
|
|
264
|
+
* deep-watches what it holds, so it walked the whole app on every sync, as far
|
|
265
|
+
* as the dev-only `app.config.compilerOptions` getter that warns when read
|
|
266
|
+
* (#1457). Read through descriptors, so no getter runs to find out.
|
|
267
|
+
*/
|
|
268
|
+
export function holdsVueApp(value: unknown): boolean {
|
|
269
|
+
if (!isObject(value)) {
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
if (isVueApp(value)) {
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
return Object.values(Object.getOwnPropertyDescriptors(value)).some(descriptor => 'value' in descriptor && isVueApp(descriptor.value))
|
|
276
|
+
}
|
|
277
|
+
|
|
237
278
|
export function useInstance(name: string): ComponentInternalInstance {
|
|
238
279
|
const vm = getCurrentInstance()
|
|
239
280
|
if (!vm) {
|
|
@@ -241,3 +282,27 @@ export function useInstance(name: string): ComponentInternalInstance {
|
|
|
241
282
|
}
|
|
242
283
|
return vm
|
|
243
284
|
}
|
|
285
|
+
|
|
286
|
+
let warnedForeignVue = false
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Warns, once, when a component built against `@poveste/vendors/vue` is
|
|
290
|
+
* rendered by another copy of Vue.
|
|
291
|
+
*
|
|
292
|
+
* The other copy runs the component's `setup`, so the vendored one has no
|
|
293
|
+
* current instance and drops every hook: the story renders nothing, and no build
|
|
294
|
+
* of Vue says why. A test mounting `MountStory` with `createApp` from `vue` read
|
|
295
|
+
* it as a story with no variants (#940).
|
|
296
|
+
*/
|
|
297
|
+
export function warnIfForeignVue(component: string) {
|
|
298
|
+
if (warnedForeignVue || _getCurrentInstance()) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
warnedForeignVue = true
|
|
302
|
+
console.warn(`[poveste] ${component} was rendered by a copy of Vue other than \`@poveste/vendors/vue\`, so it renders nothing. Poveste's client components are built against that copy: create the app with \`createApp\` from \`@poveste/vendors/vue\`, not from \`vue\`.`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** For specs: lets the warning be seen again. */
|
|
306
|
+
export function resetForeignVueWarning() {
|
|
307
|
+
warnedForeignVue = false
|
|
308
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ServerStory, ServerVariant } from '@poveste/shared'
|
|
2
|
-
import type { PropType } from 'vue'
|
|
1
|
+
import type { AutoDocsSection, ServerStory, ServerVariant } from '@poveste/shared'
|
|
2
|
+
import type { Component, PropType } from 'vue'
|
|
3
3
|
import { defineComponent, onMounted } from 'vue'
|
|
4
4
|
import { addStoryContext, addVariantContext, serverStoryContext, storyFileContext } from './context.js'
|
|
5
5
|
import { autoStubComponents } from './stub.js'
|
|
@@ -45,6 +45,18 @@ export default defineComponent({
|
|
|
45
45
|
default: false,
|
|
46
46
|
},
|
|
47
47
|
|
|
48
|
+
// A default, so an absent prop stays `undefined` rather than Vue's `false` for a Boolean.
|
|
49
|
+
autoDocs: {
|
|
50
|
+
type: [Boolean, Array] as PropType<boolean | AutoDocsSection[]>,
|
|
51
|
+
default: undefined,
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Read from the source by auto-docs, not at collection (#1432).
|
|
55
|
+
component: {
|
|
56
|
+
type: [Object, Function, Array] as PropType<Component | Component[]>,
|
|
57
|
+
default: undefined,
|
|
58
|
+
},
|
|
59
|
+
|
|
48
60
|
meta: {
|
|
49
61
|
type: Object as PropType<ServerStory['meta']>,
|
|
50
62
|
default: undefined,
|
|
@@ -70,6 +82,7 @@ export default defineComponent({
|
|
|
70
82
|
icon: props.icon,
|
|
71
83
|
iconColor: props.iconColor,
|
|
72
84
|
docsOnly: props.docsOnly,
|
|
85
|
+
...props.autoDocs !== undefined ? { autoDocs: props.autoDocs } : {},
|
|
73
86
|
meta: props.meta,
|
|
74
87
|
variants: [],
|
|
75
88
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ServerRunPayload, ServerStory } from '@poveste/shared'
|
|
2
|
+
import type { App, Component } from 'vue'
|
|
3
|
+
import type { Vue3StorySetupApi, Vue3StorySetupHandler } from '../../helpers.js'
|
|
4
|
+
import { getSetupHook } from '@poveste/shared'
|
|
5
|
+
// @ts-expect-error virtual module id
|
|
6
|
+
import * as generatedSetup from 'virtual:$poveste-generated-global-setup'
|
|
7
|
+
// @ts-expect-error virtual module id
|
|
8
|
+
import * as setup from 'virtual:$poveste-setup'
|
|
9
|
+
import { createApp, h } from 'vue'
|
|
10
|
+
import { VUE_SETUP_HOOK_NAMES } from '../../setup-hooks.js'
|
|
11
|
+
import { addStoryContext, storyFileContext } from './context.js'
|
|
12
|
+
import Story from './Story.js'
|
|
13
|
+
import Variant from './Variant.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Collects a story file's component that is already imported. Story tests get
|
|
17
|
+
* it from their own import, so no `moduleId` has to be a URL in the browser.
|
|
18
|
+
*/
|
|
19
|
+
export async function collectComponent(
|
|
20
|
+
Comp: Component & { doc?: string },
|
|
21
|
+
{ file, storyData, el }: ServerRunPayload,
|
|
22
|
+
hooks: {
|
|
23
|
+
/** Before the setup hooks, so what they report is seen. */
|
|
24
|
+
beforeSetup?: (app: App) => void
|
|
25
|
+
/** After them, so what it adds fills gaps rather than replacing the app's own. */
|
|
26
|
+
afterSetup?: (app: App) => void
|
|
27
|
+
} = {},
|
|
28
|
+
) {
|
|
29
|
+
const app = createApp({
|
|
30
|
+
// Provided from `setup` rather than the options object so the keys can be
|
|
31
|
+
// the symbols the components inject; the options form takes string keys.
|
|
32
|
+
setup() {
|
|
33
|
+
addStoryContext.provide((data: ServerStory) => {
|
|
34
|
+
storyData.push(data)
|
|
35
|
+
})
|
|
36
|
+
storyFileContext.provide(file)
|
|
37
|
+
},
|
|
38
|
+
render() {
|
|
39
|
+
return h(Comp)
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
app.component('Story', Story)
|
|
44
|
+
|
|
45
|
+
app.component('Variant', Variant)
|
|
46
|
+
|
|
47
|
+
hooks.beforeSetup?.(app)
|
|
48
|
+
|
|
49
|
+
// Call app setups to resolve global assets such as components
|
|
50
|
+
|
|
51
|
+
const setupApi: Vue3StorySetupApi = {
|
|
52
|
+
app,
|
|
53
|
+
// No story or variant exists while collecting.
|
|
54
|
+
story: undefined,
|
|
55
|
+
variant: undefined,
|
|
56
|
+
addWrapper: () => { /* noop */ },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const generatedSetupFn = getSetupHook<Vue3StorySetupHandler>(generatedSetup, VUE_SETUP_HOOK_NAMES)
|
|
60
|
+
if (generatedSetupFn) {
|
|
61
|
+
await generatedSetupFn(setupApi)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const setupFn = getSetupHook<Vue3StorySetupHandler>(setup, VUE_SETUP_HOOK_NAMES)
|
|
65
|
+
if (setupFn) {
|
|
66
|
+
await setupFn(setupApi)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
hooks.afterSetup?.(app)
|
|
70
|
+
|
|
71
|
+
app.mount(el)
|
|
72
|
+
|
|
73
|
+
if (Comp.doc) {
|
|
74
|
+
const el = document.createElement('div')
|
|
75
|
+
el.innerHTML = Comp.doc
|
|
76
|
+
const text = el.textContent
|
|
77
|
+
storyData.forEach((s) => {
|
|
78
|
+
s.docsText = text
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
app.unmount()
|
|
83
|
+
}
|
package/src/client/server/run.ts
CHANGED
|
@@ -1,67 +1,7 @@
|
|
|
1
|
-
import type { ServerRunPayload
|
|
2
|
-
import
|
|
3
|
-
import { getSetupHook } from '@poveste/shared'
|
|
4
|
-
// @ts-expect-error virtual module id
|
|
5
|
-
import * as generatedSetup from 'virtual:$poveste-generated-global-setup'
|
|
6
|
-
// @ts-expect-error virtual module id
|
|
7
|
-
import * as setup from 'virtual:$poveste-setup'
|
|
8
|
-
import { createApp, h } from 'vue'
|
|
9
|
-
import { VUE_SETUP_HOOK_NAMES } from '../../setup-hooks.js'
|
|
10
|
-
import { addStoryContext, storyFileContext } from './context.js'
|
|
11
|
-
import Story from './Story.js'
|
|
12
|
-
import Variant from './Variant.js'
|
|
1
|
+
import type { ServerRunPayload } from '@poveste/shared'
|
|
2
|
+
import { collectComponent } from './collect.js'
|
|
13
3
|
|
|
14
4
|
export async function run({ file, storyData, el }: ServerRunPayload) {
|
|
15
5
|
const { default: Comp } = await import(/* @vite-ignore */ file.moduleId)
|
|
16
|
-
|
|
17
|
-
const app = createApp({
|
|
18
|
-
// Provided from `setup` rather than the options object so the keys can be
|
|
19
|
-
// the symbols the components inject; the options form takes string keys.
|
|
20
|
-
setup() {
|
|
21
|
-
addStoryContext.provide((data: ServerStory) => {
|
|
22
|
-
storyData.push(data)
|
|
23
|
-
})
|
|
24
|
-
storyFileContext.provide(file)
|
|
25
|
-
},
|
|
26
|
-
render() {
|
|
27
|
-
return h(Comp)
|
|
28
|
-
},
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
app.component('Story', Story)
|
|
32
|
-
|
|
33
|
-
app.component('Variant', Variant)
|
|
34
|
-
|
|
35
|
-
// Call app setups to resolve global assets such as components
|
|
36
|
-
|
|
37
|
-
const setupApi: Vue3StorySetupApi = {
|
|
38
|
-
app,
|
|
39
|
-
// No story or variant exists while collecting.
|
|
40
|
-
story: undefined,
|
|
41
|
-
variant: undefined,
|
|
42
|
-
addWrapper: () => { /* noop */ },
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const generatedSetupFn = getSetupHook<Vue3StorySetupHandler>(generatedSetup, VUE_SETUP_HOOK_NAMES)
|
|
46
|
-
if (generatedSetupFn) {
|
|
47
|
-
await generatedSetupFn(setupApi)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const setupFn = getSetupHook<Vue3StorySetupHandler>(setup, VUE_SETUP_HOOK_NAMES)
|
|
51
|
-
if (setupFn) {
|
|
52
|
-
await setupFn(setupApi)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
app.mount(el)
|
|
56
|
-
|
|
57
|
-
if (Comp.doc) {
|
|
58
|
-
const el = document.createElement('div')
|
|
59
|
-
el.innerHTML = Comp.doc
|
|
60
|
-
const text = el.textContent
|
|
61
|
-
storyData.forEach((s) => {
|
|
62
|
-
s.docsText = text
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
app.unmount()
|
|
6
|
+
await collectComponent(Comp, { file, storyData, el })
|
|
67
7
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
export interface EventDoc {
|
|
4
|
+
description: string
|
|
5
|
+
tags: { name: string, text?: string }[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* vue-component-meta reads an event's JSDoc off the component's `$emit` signatures,
|
|
10
|
+
* and those are Vue's own `EmitFn` in `runtime-core.d.ts`, built from the declared
|
|
11
|
+
* type: the comment is never on them, so every event came back without a
|
|
12
|
+
* description or a tag (#1462). Volar keeps the `defineEmits` type argument, comments
|
|
13
|
+
* and all, as a `__VLS_Emit` alias in the generated code, and this reads it there,
|
|
14
|
+
* through the same program, so an emits type imported from another file resolves too.
|
|
15
|
+
*/
|
|
16
|
+
// Not only at the top: a `generic` setup, or a `<script>` block beside `<script setup>`,
|
|
17
|
+
// puts the setup, and the alias with it, inside the component's initializer.
|
|
18
|
+
function findEmitAlias(typescript: typeof ts, node: ts.Node): ts.TypeAliasDeclaration | undefined {
|
|
19
|
+
if (typescript.isTypeAliasDeclaration(node) && node.name.text === '__VLS_Emit') {
|
|
20
|
+
return node
|
|
21
|
+
}
|
|
22
|
+
return typescript.forEachChild(node, child => findEmitAlias(typescript, child))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readEventDocs(typescript: typeof ts, program: ts.Program | undefined, file: string): Map<string, EventDoc> {
|
|
26
|
+
const docs = new Map<string, EventDoc>()
|
|
27
|
+
const source = program?.getSourceFile(file)
|
|
28
|
+
const alias = source && findEmitAlias(typescript, source)
|
|
29
|
+
if (!program || !alias) {
|
|
30
|
+
return docs
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const checker = program.getTypeChecker()
|
|
34
|
+
const type = checker.getTypeAtLocation(alias.name)
|
|
35
|
+
const tagsOf = (tags: ts.JSDocTagInfo[]) => tags.map(({ name, text }) => text?.length ? { name, text: typescript.displayPartsToString(text) } : { name })
|
|
36
|
+
|
|
37
|
+
// `change: [value: string]`
|
|
38
|
+
for (const property of type.getProperties()) {
|
|
39
|
+
docs.set(property.name, {
|
|
40
|
+
description: typescript.displayPartsToString(property.getDocumentationComment(checker)),
|
|
41
|
+
tags: tagsOf(property.getJsDocTags(checker)),
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
// `(event: 'change', value: string): void`
|
|
45
|
+
for (const signature of type.getCallSignatures()) {
|
|
46
|
+
const event = signature.parameters[0] && checker.getTypeOfSymbol(signature.parameters[0])
|
|
47
|
+
if (event?.isStringLiteral()) {
|
|
48
|
+
docs.set(event.value, {
|
|
49
|
+
description: typescript.displayPartsToString(signature.getDocumentationComment(checker)),
|
|
50
|
+
tags: tagsOf(signature.getJsDocTags()),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return docs
|
|
55
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { DocgenExtractor, DocgenExtractorContext } from '@poveste/shared'
|
|
2
|
+
import type ts from 'typescript'
|
|
3
|
+
import type { VueDocgenOptions } from './meta.js'
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
import { findTsconfig } from 'poveste/docgen'
|
|
7
|
+
import { readEventDocs } from './events.js'
|
|
8
|
+
import { readGeneric } from './generic.js'
|
|
9
|
+
import { toComponentDoc } from './meta.js'
|
|
10
|
+
import { readSlotProps } from './slots.js'
|
|
11
|
+
|
|
12
|
+
const SOURCE = /\.(?:vue|[cm]?[jt]sx?)$/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs in the docgen worker. `vue-component-meta` is imported here rather than at
|
|
16
|
+
* the top, so a book without TypeScript fails this one request, answered with its
|
|
17
|
+
* error, and keeps its runtime props.
|
|
18
|
+
*/
|
|
19
|
+
export async function createExtractor(context: DocgenExtractorContext): Promise<DocgenExtractor> {
|
|
20
|
+
const { createChecker, createCheckerByJson, TypeMeta } = await import('vue-component-meta')
|
|
21
|
+
const options = (context.options ?? {}) as VueDocgenOptions
|
|
22
|
+
const checkerOptions = { forceUseTs: true, printer: { newLine: 1 } }
|
|
23
|
+
const tsconfig = findTsconfig(context.root)
|
|
24
|
+
// The copy vue-component-meta loaded, so the syntax kinds match the program it builds.
|
|
25
|
+
const typescript: typeof ts = createRequire(createRequire(import.meta.url).resolve('vue-component-meta'))('typescript')
|
|
26
|
+
const checker = tsconfig
|
|
27
|
+
? createChecker(tsconfig, checkerOptions)
|
|
28
|
+
: createCheckerByJson(context.root, { include: ['**/*'], exclude: ['node_modules'] }, checkerOptions)
|
|
29
|
+
|
|
30
|
+
// A script file is offered for its default export, which is a component only when it is
|
|
31
|
+
// `defineComponent`'s: a function, a helper or a plain object throws or reads as Unknown.
|
|
32
|
+
function scriptComponent(file: string) {
|
|
33
|
+
try {
|
|
34
|
+
const meta = checker.getComponentMeta(file, 'default')
|
|
35
|
+
return meta.type === TypeMeta.Unknown ? undefined : meta
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
extract(file) {
|
|
44
|
+
const meta = file.endsWith('.vue') ? checker.getComponentMeta(file) : scriptComponent(file)
|
|
45
|
+
if (!meta) {
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
const doc = toComponentDoc(meta, options, meta.events.length ? readEventDocs(typescript, checker.getProgram(), file) : undefined, slot => readSlotProps(typescript, checker.getProgram(), slot))
|
|
49
|
+
const generic = file.endsWith('.vue') ? readGeneric(readFileSync(file, 'utf8')) : undefined
|
|
50
|
+
return generic ? { ...doc, generic } : doc
|
|
51
|
+
},
|
|
52
|
+
update(file) {
|
|
53
|
+
if (!SOURCE.test(file)) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (existsSync(file)) {
|
|
57
|
+
checker.updateFile(file, readFileSync(file, 'utf8'))
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
checker.deleteFile(file)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
sources: () => checker.getProgram()?.getSourceFiles().map(sourceFile => sourceFile.fileName).filter(file => !file.includes('/node_modules/')) ?? [],
|
|
64
|
+
// The program is released with the checker; this drops what it cached.
|
|
65
|
+
dispose: () => checker.clearCache(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A tag's attributes may hold a `>`, as `generic="T extends Record<string, unknown>"` does,
|
|
2
|
+
// so a quoted value is read whole rather than ending the tag.
|
|
3
|
+
const SCRIPT_TAG = /<script\b((?:[^>"']|"[^"]*"|'[^']*')*)>/g
|
|
4
|
+
const SETUP = /(?:^|\s)setup(?=[\s=]|$)/
|
|
5
|
+
const GENERIC = /(?:^|\s)generic\s*=\s*(?:"([^"]*)"|'([^']*)')/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The type parameters a `<script setup generic="…">` declares, constraints and defaults
|
|
9
|
+
* included, so a table that prints `T | T[]` can say what `T` may be (#1436).
|
|
10
|
+
*/
|
|
11
|
+
export function readGeneric(source: string): string | undefined {
|
|
12
|
+
for (const [, attributes] of source.matchAll(SCRIPT_TAG)) {
|
|
13
|
+
if (SETUP.test(attributes!)) {
|
|
14
|
+
const found = GENERIC.exec(attributes!)
|
|
15
|
+
const generic = (found?.[1] ?? found?.[2])?.replace(/\s+/g, ' ').trim()
|
|
16
|
+
return generic || undefined
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ComponentDoc, DocEvent, DocProp, DocSlot, DocTag } from '@poveste/shared'
|
|
2
|
+
import type { DocgenOptions } from 'poveste/docgen'
|
|
3
|
+
import type { ComponentMeta, EventMeta, PropertyMeta, SlotMeta } from 'vue-component-meta'
|
|
4
|
+
import type { EventDoc } from './events.js'
|
|
5
|
+
import { declaredOnlyInPackages, DEFAULT_TAGS, HIDDEN_TAGS, normalizeDefault } from 'poveste/docgen'
|
|
6
|
+
|
|
7
|
+
export type VueDocgenOptions = DocgenOptions
|
|
8
|
+
|
|
9
|
+
type Tags = { name: string, text?: string }[]
|
|
10
|
+
|
|
11
|
+
function tagsOf(tags: Tags): DocTag[] {
|
|
12
|
+
return tags.map(({ name, text }) => text === undefined ? { name } : { name, text })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isHidden(tags: Tags) {
|
|
16
|
+
return tags.some(tag => HIDDEN_TAGS.includes(tag.name))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isInherited(prop: PropertyMeta, allow: string[]) {
|
|
20
|
+
return declaredOnlyInPackages(prop.getDeclarations().map(declaration => declaration.file), allow)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toProp(prop: PropertyMeta): DocProp {
|
|
24
|
+
const defaultTag = prop.tags.find(tag => DEFAULT_TAGS.includes(tag.name))?.text
|
|
25
|
+
return {
|
|
26
|
+
name: prop.name,
|
|
27
|
+
...prop.description ? { description: prop.description } : {},
|
|
28
|
+
type: prop.type,
|
|
29
|
+
required: prop.required,
|
|
30
|
+
...prop.default !== undefined ? { default: prop.default } : {},
|
|
31
|
+
...defaultTag !== undefined ? { defaultTag } : {},
|
|
32
|
+
// The code's default is what runs, so it is the one shown; the flag is for the reader.
|
|
33
|
+
...prop.default !== undefined && defaultTag !== undefined && normalizeDefault(prop.default) !== normalizeDefault(defaultTag)
|
|
34
|
+
? { defaultConflict: true }
|
|
35
|
+
: {},
|
|
36
|
+
tags: tagsOf(prop.tags),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toMember(member: Pick<SlotMeta | EventMeta, 'name' | 'description' | 'type' | 'tags'>): DocSlot & DocEvent {
|
|
41
|
+
return {
|
|
42
|
+
name: member.name,
|
|
43
|
+
...member.description ? { description: member.description } : {},
|
|
44
|
+
...member.type ? { type: member.type } : {},
|
|
45
|
+
tags: tagsOf(member.tags),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An event's payload, which vue-component-meta prints as the tuple it is declared as,
|
|
51
|
+
* `[event: KeyboardEvent]`, written as the parameter list a handler takes (#1436). Only
|
|
52
|
+
* when the outer brackets enclose the whole of it: `[a: A] | [b: B]` is left as it is.
|
|
53
|
+
*/
|
|
54
|
+
export function asParameters(type: string): string {
|
|
55
|
+
if (!type.startsWith('[')) {
|
|
56
|
+
return type
|
|
57
|
+
}
|
|
58
|
+
let depth = 0
|
|
59
|
+
for (let i = 0; i < type.length; i++) {
|
|
60
|
+
if ('[(<{'.includes(type[i]!)) {
|
|
61
|
+
depth++
|
|
62
|
+
}
|
|
63
|
+
// The `>` of an arrow, `() => void`, closes nothing.
|
|
64
|
+
else if (']>)}'.includes(type[i]!) && !(type[i] === '>' && type[i - 1] === '=') && --depth === 0) {
|
|
65
|
+
return i === type.length - 1 ? `(${type.slice(1, -1)})` : type
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return type
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// What vue-component-meta returns for an event is empty wherever its own reading is (#1462).
|
|
72
|
+
function withDocs(event: EventMeta, doc: EventDoc | undefined): Pick<EventMeta, 'name' | 'description' | 'type' | 'tags'> {
|
|
73
|
+
return {
|
|
74
|
+
name: event.name,
|
|
75
|
+
description: event.description || doc?.description || '',
|
|
76
|
+
type: asParameters(event.type),
|
|
77
|
+
tags: event.tags.length ? event.tags : doc?.tags ?? [],
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toSlot(slot: SlotMeta, slotProps: (slot: SlotMeta) => DocSlot['props'] & {}): DocSlot {
|
|
82
|
+
const props = slotProps(slot)
|
|
83
|
+
return { ...toMember(slot), ...props.length ? { props } : {} }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function toComponentDoc(meta: ComponentMeta, options: VueDocgenOptions = {}, eventDocs = new Map<string, EventDoc>(), slotProps: (slot: SlotMeta) => DocSlot['props'] & {} = () => []): ComponentDoc {
|
|
87
|
+
const allow = options.allow ?? []
|
|
88
|
+
return {
|
|
89
|
+
props: meta.props.filter(prop => !prop.global && !isHidden(prop.tags) && !isInherited(prop, allow)).map(toProp),
|
|
90
|
+
slots: meta.slots.filter(slot => !isHidden(slot.tags)).map(slot => toSlot(slot, slotProps)),
|
|
91
|
+
events: meta.events.map(event => withDocs(event, eventDocs.get(event.name))).filter(event => !isHidden(event.tags)).map(toMember),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DocSlot } from '@poveste/shared'
|
|
2
|
+
import type ts from 'typescript'
|
|
3
|
+
import type { SlotMeta } from 'vue-component-meta'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* vue-component-meta gives a slot's props as one type string, `{ open: boolean; close:
|
|
7
|
+
* () => void; }`, with the JSDoc on each of them lost inside it, and reka-ui writes its
|
|
8
|
+
* slot documentation there and nowhere else (#1436). The slot's type object resolves
|
|
9
|
+
* through the same program, one property each.
|
|
10
|
+
*/
|
|
11
|
+
type DocSlotProp = NonNullable<DocSlot['props']>[number]
|
|
12
|
+
|
|
13
|
+
export function readSlotProps(typescript: typeof ts, program: ts.Program | undefined, slot: SlotMeta): DocSlotProp[] {
|
|
14
|
+
if (!program) {
|
|
15
|
+
return []
|
|
16
|
+
}
|
|
17
|
+
const checker = program.getTypeChecker()
|
|
18
|
+
let type: ts.Type
|
|
19
|
+
try {
|
|
20
|
+
type = slot.getTypeObject()
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return []
|
|
24
|
+
}
|
|
25
|
+
return type.getProperties().map((property) => {
|
|
26
|
+
const description = typescript.displayPartsToString(property.getDocumentationComment(checker))
|
|
27
|
+
return {
|
|
28
|
+
name: property.name,
|
|
29
|
+
type: checker.typeToString(checker.getTypeOfSymbol(property)),
|
|
30
|
+
...description ? { description } : {},
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
package/src/index.node.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { Plugin } from 'poveste'
|
|
2
2
|
|
|
3
|
-
import { readFileSync } from 'node:fs'
|
|
4
3
|
import { createRequire } from 'node:module'
|
|
5
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
6
5
|
|
|
6
|
+
import { CLIENT_DEPENDENCIES } from './client-dependencies.js'
|
|
7
7
|
import generateStoryCommand from './commands/generate-story.server.js'
|
|
8
8
|
import { VUE_SETUP_HOOK_NAMES } from './setup-hooks.js'
|
|
9
9
|
import { listComponentFiles } from './util/list-components.js'
|
|
10
|
+
import { projectVueVersion } from './util/project-vue.js'
|
|
10
11
|
import { VAPOR_INTEROP_ID, vaporInteropModule, withoutVaporPlugin } from './util/vapor.js'
|
|
12
|
+
import { requireVuePlugin } from './util/vue-plugin.js'
|
|
13
|
+
|
|
14
|
+
const SCRIPT_COMPONENT = /(?<!\.d)\.[cm]?[jt]sx?$/
|
|
11
15
|
|
|
12
16
|
export function HstVue(): Plugin {
|
|
13
17
|
return {
|
|
@@ -28,7 +32,16 @@ export function HstVue(): Plugin {
|
|
|
28
32
|
config() {
|
|
29
33
|
return {
|
|
30
34
|
vite: {
|
|
35
|
+
// The chrome's client code imports these, and the scan never reaches them: it
|
|
36
|
+
// loads through virtual modules, so a cold `poveste dev` found them only when the
|
|
37
|
+
// page asked, re-bundled and reloaded a Nuxt reader's first page (#1477). Named
|
|
38
|
+
// through this package, which every Vue, Nuxt and Quasar book lists, because the
|
|
39
|
+
// book itself cannot resolve them under pnpm.
|
|
40
|
+
optimizeDeps: {
|
|
41
|
+
include: CLIENT_DEPENDENCIES.map(name => `@poveste/plugin-vue > ${name}`),
|
|
42
|
+
},
|
|
31
43
|
plugins: [
|
|
44
|
+
requireVuePlugin(),
|
|
32
45
|
withoutVaporPlugin(),
|
|
33
46
|
(() => {
|
|
34
47
|
let root = process.cwd()
|
|
@@ -59,6 +72,16 @@ export function HstVue(): Plugin {
|
|
|
59
72
|
}
|
|
60
73
|
},
|
|
61
74
|
|
|
75
|
+
// A story file documents nothing: it names the components that do. A script file is
|
|
76
|
+
// offered too, for a component written with `defineComponent` (#1463); the extractor
|
|
77
|
+
// answers nothing for one that isn't.
|
|
78
|
+
docgen: {
|
|
79
|
+
match: file => (file.endsWith('.vue') || SCRIPT_COMPONENT.test(file)) && !/\.story\.[^/]+$/.test(file) && !file.includes('/node_modules/'),
|
|
80
|
+
module: fileURLToPath(new URL('./docgen/extractor.js', import.meta.url)),
|
|
81
|
+
// `vue-component-meta` requires `typescript` from where it is installed, an optional peer npm does not add.
|
|
82
|
+
typescript: createRequire(import.meta.url).resolve('vue-component-meta'),
|
|
83
|
+
},
|
|
84
|
+
|
|
62
85
|
supportPlugin: {
|
|
63
86
|
id: 'vue3',
|
|
64
87
|
moduleName: '@poveste/plugin-vue',
|
|
@@ -82,14 +105,6 @@ export function HstVue(): Plugin {
|
|
|
82
105
|
}
|
|
83
106
|
}
|
|
84
107
|
|
|
85
|
-
function projectVueVersion(root: string): string | undefined {
|
|
86
|
-
try {
|
|
87
|
-
const manifest = createRequire(join(root, 'package.json')).resolve('vue/package.json')
|
|
88
|
-
return JSON.parse(readFileSync(manifest, 'utf8')).version
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return undefined
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
108
|
export * from './helpers.js'
|
|
109
|
+
export { povesteTesting } from './testing/vite.js'
|
|
110
|
+
export type { PovesteTestingOptions } from './testing/vite.js'
|