@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.
Files changed (57) hide show
  1. package/README.md +7 -0
  2. package/dist/bundled/client/app/Story.js +60 -48
  3. package/dist/bundled/client/app/global-components.js +5 -3
  4. package/dist/bundled/client/app/host.js +9 -5
  5. package/dist/bundled/client/app/util.js +13 -4
  6. package/dist/bundled/client/server/Story.js +13 -0
  7. package/dist/bundled/client/server/collect.js +41 -0
  8. package/dist/bundled/client/server/run.js +10 -39
  9. package/dist/bundled/testing/index.js +168 -0
  10. package/dist/client/app/Story.d.ts +19 -1
  11. package/dist/client/app/Variant.d.ts +2 -2
  12. package/dist/client/app/global-components.d.ts +1 -0
  13. package/dist/client/app/host.d.ts +8 -0
  14. package/dist/client/app/util.d.ts +16 -0
  15. package/dist/client/server/Story.d.ts +21 -3
  16. package/dist/client/server/collect.d.ts +14 -0
  17. package/dist/client-dependencies.d.ts +2 -0
  18. package/dist/client-dependencies.js +2 -0
  19. package/dist/docgen/events.d.ts +9 -0
  20. package/dist/docgen/events.js +45 -0
  21. package/dist/docgen/extractor.d.ts +0 -1
  22. package/dist/docgen/extractor.js +28 -9
  23. package/dist/docgen/generic.d.ts +5 -0
  24. package/dist/docgen/generic.js +19 -0
  25. package/dist/docgen/meta.d.ts +12 -9
  26. package/dist/docgen/meta.js +39 -13
  27. package/dist/docgen/slots.d.ts +6 -0
  28. package/dist/docgen/slots.js +21 -0
  29. package/dist/index.node.d.ts +2 -0
  30. package/dist/index.node.js +18 -15
  31. package/dist/testing/index.d.ts +28 -0
  32. package/dist/testing/vite.d.ts +18 -0
  33. package/dist/testing/vite.js +171 -0
  34. package/dist/util/project-vue.d.ts +1 -0
  35. package/dist/util/project-vue.js +12 -0
  36. package/package.json +26 -9
  37. package/src/client/app/Story.ts +15 -3
  38. package/src/client/app/global-components.ts +4 -0
  39. package/src/client/app/host.ts +21 -2
  40. package/src/client/app/util.ts +40 -0
  41. package/src/client/server/Story.ts +15 -2
  42. package/src/client/server/collect.ts +83 -0
  43. package/src/client/server/run.ts +3 -63
  44. package/src/client-dependencies.ts +2 -0
  45. package/src/docgen/events.ts +55 -0
  46. package/src/docgen/extractor.ts +30 -10
  47. package/src/docgen/generic.ts +20 -0
  48. package/src/docgen/meta.ts +46 -21
  49. package/src/docgen/slots.ts +33 -0
  50. package/src/index.node.ts +20 -16
  51. package/src/testing/index.ts +286 -0
  52. package/src/testing/vite.ts +194 -0
  53. package/src/util/project-vue.ts +13 -0
  54. package/testing.d.ts +2 -0
  55. package/dist/docgen/typescript.d.ts +0 -9
  56. package/dist/docgen/typescript.js +0 -36
  57. 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.20.0",
5
- "description": "Poveste plugin for Vue.js support",
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.20.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.20.0",
80
- "@poveste/shared": "^0.20.0",
81
- "@poveste/vendors": "^0.20.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
- "poveste": "0.20.0",
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.4",
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
  }
@@ -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 (typeof value === 'function' || (value?.__file) || typeof value?.render === 'function' || typeof value?.setup === 'function') {
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
  }
@@ -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
- console.error(err)
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
 
@@ -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
+ }
@@ -1,67 +1,7 @@
1
- import type { ServerRunPayload, ServerStory } from '@poveste/shared'
2
- import type { Vue3StorySetupApi, Vue3StorySetupHandler } from '../../helpers.js'
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,2 @@
1
+ /** What the client code imports from npm, for the optimizer to bundle up front. */
2
+ export const CLIENT_DEPENDENCIES = ['change-case']
@@ -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
+ }
@@ -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 { join } from 'pathe'
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: file => toComponentDoc(checker.getComponentMeta(file), options),
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
+ }