@poveste/plugin-vue 0.20.0 → 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/Story.js +60 -48
- 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 +13 -4
- 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/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 +16 -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 +0 -1
- package/dist/docgen/extractor.js +28 -9
- package/dist/docgen/generic.d.ts +5 -0
- package/dist/docgen/generic.js +19 -0
- package/dist/docgen/meta.d.ts +12 -9
- package/dist/docgen/meta.js +39 -13
- 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 +18 -15
- 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/package.json +26 -9
- 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 +40 -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 +30 -10
- package/src/docgen/generic.ts +20 -0
- package/src/docgen/meta.ts +46 -21
- package/src/docgen/slots.ts +33 -0
- package/src/index.node.ts +20 -16
- package/src/testing/index.ts +286 -0
- package/src/testing/vite.ts +194 -0
- package/src/util/project-vue.ts +13 -0
- package/testing.d.ts +2 -0
- package/dist/docgen/typescript.d.ts +0 -9
- package/dist/docgen/typescript.js +0 -36
- package/src/docgen/typescript.ts +0 -41
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function projectVueVersion(root) {
|
|
5
|
+
try {
|
|
6
|
+
const manifest = createRequire(join(root, 'package.json')).resolve('vue/package.json');
|
|
7
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).version;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poveste/plugin-vue",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Poveste plugin for Vue.
|
|
4
|
+
"version": "0.21.0",
|
|
5
|
+
"description": "Poveste plugin for Vue 3: stories as .vue files, controls, and a props, slots and events table read from your components' types",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan",
|
|
8
8
|
"url": "https://x.com/50rayn"
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./dist/client/server.d.ts",
|
|
42
42
|
"default": "./dist/bundled/client/server.js"
|
|
43
43
|
},
|
|
44
|
+
"./testing": {
|
|
45
|
+
"types": "./dist/testing/index.d.ts",
|
|
46
|
+
"default": "./dist/bundled/testing/index.js"
|
|
47
|
+
},
|
|
44
48
|
"./components": {
|
|
45
49
|
"types": "./components.d.ts"
|
|
46
50
|
},
|
|
@@ -66,19 +70,26 @@
|
|
|
66
70
|
"collect.d.ts",
|
|
67
71
|
"components.d.ts",
|
|
68
72
|
"dist",
|
|
69
|
-
"src"
|
|
73
|
+
"src",
|
|
74
|
+
"testing.d.ts"
|
|
70
75
|
],
|
|
71
76
|
"engines": {
|
|
72
77
|
"node": ">=22.22.2"
|
|
73
78
|
},
|
|
74
79
|
"peerDependencies": {
|
|
75
|
-
"poveste": "^0.
|
|
80
|
+
"poveste": "^0.21.0",
|
|
81
|
+
"vitest": "^5.0.0",
|
|
76
82
|
"vue": "^3.5.43"
|
|
77
83
|
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"vitest": {
|
|
86
|
+
"optional": true
|
|
87
|
+
}
|
|
88
|
+
},
|
|
78
89
|
"dependencies": {
|
|
79
|
-
"@poveste/controls": "^0.
|
|
80
|
-
"@poveste/shared": "^0.
|
|
81
|
-
"@poveste/vendors": "^0.
|
|
90
|
+
"@poveste/controls": "^0.21.0",
|
|
91
|
+
"@poveste/shared": "^0.21.0",
|
|
92
|
+
"@poveste/vendors": "^0.21.0",
|
|
82
93
|
"change-case": "^5.4.4",
|
|
83
94
|
"ignore": "^7.0.10",
|
|
84
95
|
"launch-editor": "^2.14.2",
|
|
@@ -87,12 +98,17 @@
|
|
|
87
98
|
"vue-component-meta": "^3.3.13"
|
|
88
99
|
},
|
|
89
100
|
"devDependencies": {
|
|
101
|
+
"@fontsource/fira-mono": "^5.3.0",
|
|
102
|
+
"@tailwindcss/oxide": "^4.3.3",
|
|
90
103
|
"@types/node": "^26.6.5",
|
|
104
|
+
"@vitejs/plugin-vue": "^6.0.10",
|
|
105
|
+
"@vitest/browser-playwright": "5.0.5",
|
|
91
106
|
"concurrently": "^10.0.6",
|
|
92
|
-
"
|
|
107
|
+
"jsdom": "^30.1.3",
|
|
108
|
+
"poveste": "0.21.0",
|
|
93
109
|
"typescript": "6.0.3",
|
|
94
110
|
"vite": "^8.3.4",
|
|
95
|
-
"vitest": "^5.0.
|
|
111
|
+
"vitest": "^5.0.5",
|
|
96
112
|
"vue": "^3.5.44",
|
|
97
113
|
"vue-vapor-rc": "npm:vue@3.6.0-rc.10"
|
|
98
114
|
},
|
|
@@ -101,6 +117,7 @@
|
|
|
101
117
|
"build:types": "tsc --declaration --emitDeclarationOnly",
|
|
102
118
|
"watch": "concurrently \"vite build --watch\" \"tsc -d -P tsconfig.build.json --watch\" \"pnpm run build:types --watch\"",
|
|
103
119
|
"test": "vitest run",
|
|
120
|
+
"test:story-tests": "vitest run --config vitest.story-tests.config.ts",
|
|
104
121
|
"test:dev": "vitest"
|
|
105
122
|
}
|
|
106
123
|
}
|
package/src/client/app/Story.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Story } from '@poveste/shared'
|
|
2
|
-
import type { PropType, VNode } from 'vue'
|
|
2
|
+
import type { Component, PropType, VNode } from 'vue'
|
|
3
3
|
import type { PreviewRenderContext } from './render-context.js'
|
|
4
4
|
import { omitInheritStoryProps } from '@poveste/shared'
|
|
5
5
|
import { cloneVNode, computed, defineComponent, h, isRef, reactive, useAttrs } from 'vue'
|
|
6
6
|
import { implicitStateContext, storyContext } from './context.js'
|
|
7
7
|
import { useRenderContext } from './render-context.js'
|
|
8
|
-
import { isWritableBinding, useInstance } from './util.js'
|
|
8
|
+
import { holdsVueApp, isComponentBinding, isWritableBinding, useInstance } from './util.js'
|
|
9
9
|
import Variant from './Variant.js'
|
|
10
10
|
|
|
11
11
|
export default defineComponent({
|
|
@@ -25,6 +25,18 @@ export default defineComponent({
|
|
|
25
25
|
type: Object as PropType<Story['meta']>,
|
|
26
26
|
default: undefined,
|
|
27
27
|
},
|
|
28
|
+
|
|
29
|
+
// Read at collection; declared so it is not passed on to the variants (#1398).
|
|
30
|
+
autoDocs: {
|
|
31
|
+
type: [Boolean, Array] as PropType<Story['autoDocs']>,
|
|
32
|
+
default: undefined,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// The components the Docs tab documents, read from the source by auto-docs (#1432).
|
|
36
|
+
component: {
|
|
37
|
+
type: [Object, Function, Array] as PropType<Component | Component[]>,
|
|
38
|
+
default: undefined,
|
|
39
|
+
},
|
|
28
40
|
},
|
|
29
41
|
|
|
30
42
|
setup(props) {
|
|
@@ -53,7 +65,7 @@ export default defineComponent({
|
|
|
53
65
|
function addImplicitState(source: Record<string, any>, key: string) {
|
|
54
66
|
const value = source[key]
|
|
55
67
|
|
|
56
|
-
if (
|
|
68
|
+
if (isComponentBinding(value) || holdsVueApp(value)) {
|
|
57
69
|
return
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -28,6 +28,10 @@ export function registerGlobalComponents(app: App) {
|
|
|
28
28
|
|
|
29
29
|
app.component('Variant', Variant)
|
|
30
30
|
|
|
31
|
+
registerControlComponents(app)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function registerControlComponents(app: App) {
|
|
31
35
|
for (const [key, component] of Object.entries(components)) {
|
|
32
36
|
app.component(key, wrapControlComponent(component))
|
|
33
37
|
}
|
package/src/client/app/host.ts
CHANGED
|
@@ -27,6 +27,13 @@ interface PreviewHostOptions {
|
|
|
27
27
|
getVariant: () => Variant | null
|
|
28
28
|
renderContext: PreviewRenderContext
|
|
29
29
|
wrapInDiv?: boolean
|
|
30
|
+
/** A story test's: what the app would only log, it fails on. */
|
|
31
|
+
onError?: (err: unknown) => void
|
|
32
|
+
onWarn?: (message: string, trace: string) => void
|
|
33
|
+
/** A story test's: when the render's async setups have settled. */
|
|
34
|
+
onResolve?: () => void
|
|
35
|
+
/** A story test's, run after the app's own registrations and setup hooks. */
|
|
36
|
+
configureApp?: (app: App) => void
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
const PreviewHostRoot = defineComponent({
|
|
@@ -86,7 +93,7 @@ export function createPreviewHost(options: PreviewHostOptions) {
|
|
|
86
93
|
vnode = h('div', vnode)
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
return h(Suspense, {}, vnode)
|
|
96
|
+
return h(Suspense, options.onResolve ? { onResolve: options.onResolve } : {}, vnode)
|
|
90
97
|
},
|
|
91
98
|
})
|
|
92
99
|
|
|
@@ -98,7 +105,17 @@ export function createPreviewHost(options: PreviewHostOptions) {
|
|
|
98
105
|
storyId: options.getStory()?.id,
|
|
99
106
|
variantId: options.getVariant()?.id,
|
|
100
107
|
})
|
|
101
|
-
|
|
108
|
+
if (options.onError) {
|
|
109
|
+
options.onError(err)
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.error(err)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (options.onWarn) {
|
|
117
|
+
const onWarn = options.onWarn
|
|
118
|
+
app.config.warnHandler = (message, _instance, trace) => onWarn(message, trace)
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
// From Vue 3.6, a Vapor component inside a VDOM tree mounts only through this
|
|
@@ -123,6 +140,8 @@ export function createPreviewHost(options: PreviewHostOptions) {
|
|
|
123
140
|
|
|
124
141
|
await runSetupHooks(setupApi)
|
|
125
142
|
|
|
143
|
+
options.configureApp?.(app)
|
|
144
|
+
|
|
126
145
|
app.mount(target)
|
|
127
146
|
}
|
|
128
147
|
|
package/src/client/app/util.ts
CHANGED
|
@@ -235,6 +235,46 @@ export function isWritableBinding(value: unknown): boolean {
|
|
|
235
235
|
return !(isRef(value) && isReadonly(value))
|
|
236
236
|
}
|
|
237
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
|
+
|
|
238
278
|
export function useInstance(name: string): ComponentInternalInstance {
|
|
239
279
|
const vm = getCurrentInstance()
|
|
240
280
|
if (!vm) {
|
|
@@ -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
|
+
}
|
package/src/docgen/extractor.ts
CHANGED
|
@@ -1,34 +1,54 @@
|
|
|
1
1
|
import type { DocgenExtractor, DocgenExtractorContext } from '@poveste/shared'
|
|
2
|
+
import type ts from 'typescript'
|
|
2
3
|
import type { VueDocgenOptions } from './meta.js'
|
|
3
4
|
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
-
import {
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
import { findTsconfig } from 'poveste/docgen'
|
|
7
|
+
import { readEventDocs } from './events.js'
|
|
8
|
+
import { readGeneric } from './generic.js'
|
|
5
9
|
import { toComponentDoc } from './meta.js'
|
|
10
|
+
import { readSlotProps } from './slots.js'
|
|
6
11
|
|
|
7
|
-
// `tsconfig.app.json` first: Vite's Vue template splits the app's settings into it
|
|
8
|
-
// and leaves `tsconfig.json` holding only references, which type nothing.
|
|
9
|
-
const TSCONFIGS = ['tsconfig.app.json', 'tsconfig.json']
|
|
10
12
|
const SOURCE = /\.(?:vue|[cm]?[jt]sx?)$/
|
|
11
13
|
|
|
12
|
-
export function findTsconfig(root: string) {
|
|
13
|
-
return TSCONFIGS.map(name => join(root, name)).find(file => existsSync(file))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
15
|
* Runs in the docgen worker. `vue-component-meta` is imported here rather than at
|
|
18
16
|
* the top, so a book without TypeScript fails this one request, answered with its
|
|
19
17
|
* error, and keeps its runtime props.
|
|
20
18
|
*/
|
|
21
19
|
export async function createExtractor(context: DocgenExtractorContext): Promise<DocgenExtractor> {
|
|
22
|
-
const { createChecker, createCheckerByJson } = await import('vue-component-meta')
|
|
20
|
+
const { createChecker, createCheckerByJson, TypeMeta } = await import('vue-component-meta')
|
|
23
21
|
const options = (context.options ?? {}) as VueDocgenOptions
|
|
24
22
|
const checkerOptions = { forceUseTs: true, printer: { newLine: 1 } }
|
|
25
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
26
|
const checker = tsconfig
|
|
27
27
|
? createChecker(tsconfig, checkerOptions)
|
|
28
28
|
: createCheckerByJson(context.root, { include: ['**/*'], exclude: ['node_modules'] }, checkerOptions)
|
|
29
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
|
+
|
|
30
42
|
return {
|
|
31
|
-
extract
|
|
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
|
+
},
|
|
32
52
|
update(file) {
|
|
33
53
|
if (!SOURCE.test(file)) {
|
|
34
54
|
return
|
|
@@ -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
|
+
}
|