@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.
Files changed (62) hide show
  1. package/README.md +7 -0
  2. package/dist/bundled/client/app/MountStory.js +32 -30
  3. package/dist/bundled/client/app/RenderStory.js +39 -44
  4. package/dist/bundled/client/app/Story.js +62 -60
  5. package/dist/bundled/client/app/global-components.js +5 -3
  6. package/dist/bundled/client/app/host.js +9 -5
  7. package/dist/bundled/client/app/util.js +46 -33
  8. package/dist/bundled/client/server/Story.js +13 -0
  9. package/dist/bundled/client/server/collect.js +41 -0
  10. package/dist/bundled/client/server/run.js +10 -39
  11. package/dist/bundled/client/server.js +1 -2
  12. package/dist/bundled/testing/index.js +168 -0
  13. package/dist/client/app/Story.d.ts +19 -1
  14. package/dist/client/app/Variant.d.ts +2 -2
  15. package/dist/client/app/global-components.d.ts +1 -0
  16. package/dist/client/app/host.d.ts +8 -0
  17. package/dist/client/app/util.d.ts +28 -0
  18. package/dist/client/server/Story.d.ts +21 -3
  19. package/dist/client/server/collect.d.ts +14 -0
  20. package/dist/client-dependencies.d.ts +2 -0
  21. package/dist/client-dependencies.js +2 -0
  22. package/dist/docgen/events.d.ts +9 -0
  23. package/dist/docgen/events.js +45 -0
  24. package/dist/docgen/extractor.d.ts +7 -0
  25. package/dist/docgen/extractor.js +60 -0
  26. package/dist/docgen/generic.d.ts +5 -0
  27. package/dist/docgen/generic.js +19 -0
  28. package/dist/docgen/meta.d.ts +12 -0
  29. package/dist/docgen/meta.js +76 -0
  30. package/dist/docgen/slots.d.ts +6 -0
  31. package/dist/docgen/slots.js +21 -0
  32. package/dist/index.node.d.ts +2 -0
  33. package/dist/index.node.js +24 -11
  34. package/dist/testing/index.d.ts +28 -0
  35. package/dist/testing/vite.d.ts +18 -0
  36. package/dist/testing/vite.js +171 -0
  37. package/dist/util/project-vue.d.ts +1 -0
  38. package/dist/util/project-vue.js +12 -0
  39. package/dist/util/vue-plugin.d.ts +10 -0
  40. package/dist/util/vue-plugin.js +28 -0
  41. package/package.json +33 -15
  42. package/src/client/app/MountStory.ts +2 -0
  43. package/src/client/app/RenderStory.ts +2 -1
  44. package/src/client/app/Story.ts +15 -3
  45. package/src/client/app/global-components.ts +4 -0
  46. package/src/client/app/host.ts +21 -2
  47. package/src/client/app/util.ts +65 -0
  48. package/src/client/server/Story.ts +15 -2
  49. package/src/client/server/collect.ts +83 -0
  50. package/src/client/server/run.ts +3 -63
  51. package/src/client-dependencies.ts +2 -0
  52. package/src/docgen/events.ts +55 -0
  53. package/src/docgen/extractor.ts +67 -0
  54. package/src/docgen/generic.ts +20 -0
  55. package/src/docgen/meta.ts +93 -0
  56. package/src/docgen/slots.ts +33 -0
  57. package/src/index.node.ts +27 -12
  58. package/src/testing/index.ts +286 -0
  59. package/src/testing/vite.ts +194 -0
  60. package/src/util/project-vue.ts +13 -0
  61. package/src/util/vue-plugin.ts +32 -0
  62. package/testing.d.ts +2 -0
@@ -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
+ }
@@ -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
+ }
@@ -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 { join } from 'node:path'
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'