@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
@@ -1,5 +1,5 @@
1
- import type { ServerStory } from '@poveste/shared';
2
- import type { PropType } from 'vue';
1
+ import type { AutoDocsSection, ServerStory } from '@poveste/shared';
2
+ import type { Component, PropType } from 'vue';
3
3
  declare const _default: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
4
  title: {
5
5
  type: StringConstructor;
@@ -29,6 +29,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
29
29
  type: BooleanConstructor;
30
30
  default: boolean;
31
31
  };
32
+ autoDocs: {
33
+ type: PropType<boolean | AutoDocsSection[]>;
34
+ default: undefined;
35
+ };
36
+ component: {
37
+ type: PropType<Component | Component[]>;
38
+ default: undefined;
39
+ };
32
40
  meta: {
33
41
  type: PropType<ServerStory["meta"]>;
34
42
  default: undefined;
@@ -64,18 +72,28 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
64
72
  type: BooleanConstructor;
65
73
  default: boolean;
66
74
  };
75
+ autoDocs: {
76
+ type: PropType<boolean | AutoDocsSection[]>;
77
+ default: undefined;
78
+ };
79
+ component: {
80
+ type: PropType<Component | Component[]>;
81
+ default: undefined;
82
+ };
67
83
  meta: {
68
84
  type: PropType<ServerStory["meta"]>;
69
85
  default: undefined;
70
86
  };
71
87
  }>> & Readonly<{}>, {
88
+ component: Component | Component[];
72
89
  meta: import("poveste").StoryMeta | undefined;
90
+ autoDocs: boolean | AutoDocsSection[];
73
91
  id: string;
74
92
  title: string;
75
93
  group: string;
76
94
  layout: import("poveste").StoryLayout | undefined;
95
+ docsOnly: boolean;
77
96
  icon: string;
78
97
  iconColor: string;
79
- docsOnly: boolean;
80
98
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
81
99
  export default _default;
@@ -0,0 +1,14 @@
1
+ import type { ServerRunPayload } from '@poveste/shared';
2
+ import type { App, Component } from 'vue';
3
+ /**
4
+ * Collects a story file's component that is already imported. Story tests get
5
+ * it from their own import, so no `moduleId` has to be a URL in the browser.
6
+ */
7
+ export declare function collectComponent(Comp: Component & {
8
+ doc?: string;
9
+ }, { file, storyData, el }: ServerRunPayload, hooks?: {
10
+ /** Before the setup hooks, so what they report is seen. */
11
+ beforeSetup?: (app: App) => void;
12
+ /** After them, so what it adds fills gaps rather than replacing the app's own. */
13
+ afterSetup?: (app: App) => void;
14
+ }): Promise<void>;
@@ -0,0 +1,2 @@
1
+ /** What the client code imports from npm, for the optimizer to bundle up front. */
2
+ export declare const CLIENT_DEPENDENCIES: string[];
@@ -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,9 @@
1
+ import type ts from 'typescript';
2
+ export interface EventDoc {
3
+ description: string;
4
+ tags: {
5
+ name: string;
6
+ text?: string;
7
+ }[];
8
+ }
9
+ export declare function readEventDocs(typescript: typeof ts, program: ts.Program | undefined, file: string): Map<string, EventDoc>;
@@ -0,0 +1,45 @@
1
+ /*
2
+ * vue-component-meta reads an event's JSDoc off the component's `$emit` signatures,
3
+ * and those are Vue's own `EmitFn` in `runtime-core.d.ts`, built from the declared
4
+ * type: the comment is never on them, so every event came back without a
5
+ * description or a tag (#1462). Volar keeps the `defineEmits` type argument, comments
6
+ * and all, as a `__VLS_Emit` alias in the generated code, and this reads it there,
7
+ * through the same program, so an emits type imported from another file resolves too.
8
+ */
9
+ // Not only at the top: a `generic` setup, or a `<script>` block beside `<script setup>`,
10
+ // puts the setup, and the alias with it, inside the component's initializer.
11
+ function findEmitAlias(typescript, node) {
12
+ if (typescript.isTypeAliasDeclaration(node) && node.name.text === '__VLS_Emit') {
13
+ return node;
14
+ }
15
+ return typescript.forEachChild(node, child => findEmitAlias(typescript, child));
16
+ }
17
+ export function readEventDocs(typescript, program, file) {
18
+ const docs = new Map();
19
+ const source = program?.getSourceFile(file);
20
+ const alias = source && findEmitAlias(typescript, source);
21
+ if (!program || !alias) {
22
+ return docs;
23
+ }
24
+ const checker = program.getTypeChecker();
25
+ const type = checker.getTypeAtLocation(alias.name);
26
+ const tagsOf = (tags) => tags.map(({ name, text }) => text?.length ? { name, text: typescript.displayPartsToString(text) } : { name });
27
+ // `change: [value: string]`
28
+ for (const property of type.getProperties()) {
29
+ docs.set(property.name, {
30
+ description: typescript.displayPartsToString(property.getDocumentationComment(checker)),
31
+ tags: tagsOf(property.getJsDocTags(checker)),
32
+ });
33
+ }
34
+ // `(event: 'change', value: string): void`
35
+ for (const signature of type.getCallSignatures()) {
36
+ const event = signature.parameters[0] && checker.getTypeOfSymbol(signature.parameters[0]);
37
+ if (event?.isStringLiteral()) {
38
+ docs.set(event.value, {
39
+ description: typescript.displayPartsToString(signature.getDocumentationComment(checker)),
40
+ tags: tagsOf(signature.getJsDocTags()),
41
+ });
42
+ }
43
+ }
44
+ return docs;
45
+ }
@@ -1,5 +1,4 @@
1
1
  import type { DocgenExtractor, DocgenExtractorContext } from '@poveste/shared';
2
- export declare function findTsconfig(root: string): string | undefined;
3
2
  /**
4
3
  * Runs in the docgen worker. `vue-component-meta` is imported here rather than at
5
4
  * the top, so a book without TypeScript fails this one request, answered with its
@@ -1,28 +1,47 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'pathe';
2
+ import { createRequire } from 'node:module';
3
+ import { findTsconfig } from 'poveste/docgen';
4
+ import { readEventDocs } from './events.js';
5
+ import { readGeneric } from './generic.js';
3
6
  import { toComponentDoc } from './meta.js';
4
- // `tsconfig.app.json` first: Vite's Vue template splits the app's settings into it
5
- // and leaves `tsconfig.json` holding only references, which type nothing.
6
- const TSCONFIGS = ['tsconfig.app.json', 'tsconfig.json'];
7
+ import { readSlotProps } from './slots.js';
7
8
  const SOURCE = /\.(?:vue|[cm]?[jt]sx?)$/;
8
- export function findTsconfig(root) {
9
- return TSCONFIGS.map(name => join(root, name)).find(file => existsSync(file));
10
- }
11
9
  /**
12
10
  * Runs in the docgen worker. `vue-component-meta` is imported here rather than at
13
11
  * the top, so a book without TypeScript fails this one request, answered with its
14
12
  * error, and keeps its runtime props.
15
13
  */
16
14
  export async function createExtractor(context) {
17
- const { createChecker, createCheckerByJson } = await import('vue-component-meta');
15
+ const { createChecker, createCheckerByJson, TypeMeta } = await import('vue-component-meta');
18
16
  const options = (context.options ?? {});
19
17
  const checkerOptions = { forceUseTs: true, printer: { newLine: 1 } };
20
18
  const tsconfig = findTsconfig(context.root);
19
+ // The copy vue-component-meta loaded, so the syntax kinds match the program it builds.
20
+ const typescript = createRequire(createRequire(import.meta.url).resolve('vue-component-meta'))('typescript');
21
21
  const checker = tsconfig
22
22
  ? createChecker(tsconfig, checkerOptions)
23
23
  : createCheckerByJson(context.root, { include: ['**/*'], exclude: ['node_modules'] }, checkerOptions);
24
+ // A script file is offered for its default export, which is a component only when it is
25
+ // `defineComponent`'s: a function, a helper or a plain object throws or reads as Unknown.
26
+ function scriptComponent(file) {
27
+ try {
28
+ const meta = checker.getComponentMeta(file, 'default');
29
+ return meta.type === TypeMeta.Unknown ? undefined : meta;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
24
35
  return {
25
- extract: file => toComponentDoc(checker.getComponentMeta(file), options),
36
+ extract(file) {
37
+ const meta = file.endsWith('.vue') ? checker.getComponentMeta(file) : scriptComponent(file);
38
+ if (!meta) {
39
+ return undefined;
40
+ }
41
+ const doc = toComponentDoc(meta, options, meta.events.length ? readEventDocs(typescript, checker.getProgram(), file) : undefined, slot => readSlotProps(typescript, checker.getProgram(), slot));
42
+ const generic = file.endsWith('.vue') ? readGeneric(readFileSync(file, 'utf8')) : undefined;
43
+ return generic ? { ...doc, generic } : doc;
44
+ },
26
45
  update(file) {
27
46
  if (!SOURCE.test(file)) {
28
47
  return;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The type parameters a `<script setup generic="…">` declares, constraints and defaults
3
+ * included, so a table that prints `T | T[]` can say what `T` may be (#1436).
4
+ */
5
+ export declare function readGeneric(source: string): string | undefined;
@@ -0,0 +1,19 @@
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
+ * The type parameters a `<script setup generic="…">` declares, constraints and defaults
8
+ * included, so a table that prints `T | T[]` can say what `T` may be (#1436).
9
+ */
10
+ export function readGeneric(source) {
11
+ for (const [, attributes] of source.matchAll(SCRIPT_TAG)) {
12
+ if (SETUP.test(attributes)) {
13
+ const found = GENERIC.exec(attributes);
14
+ const generic = (found?.[1] ?? found?.[2])?.replace(/\s+/g, ' ').trim();
15
+ return generic || undefined;
16
+ }
17
+ }
18
+ return undefined;
19
+ }
@@ -1,9 +1,12 @@
1
- import type { ComponentDoc } from '@poveste/shared';
2
- import type { ComponentMeta } from 'vue-component-meta';
3
- export interface VueDocgenOptions {
4
- /** Packages whose props count although they are declared under `node_modules`. */
5
- allow?: string[];
6
- }
7
- /** A default as written, without the quoting a tag or the printer added. */
8
- export declare function normalizeDefault(value: string): string;
9
- export declare function toComponentDoc(meta: ComponentMeta, options?: VueDocgenOptions): ComponentDoc;
1
+ import type { ComponentDoc, DocSlot } from '@poveste/shared';
2
+ import type { DocgenOptions } from 'poveste/docgen';
3
+ import type { ComponentMeta, SlotMeta } from 'vue-component-meta';
4
+ import type { EventDoc } from './events.js';
5
+ export type VueDocgenOptions = DocgenOptions;
6
+ /**
7
+ * An event's payload, which vue-component-meta prints as the tuple it is declared as,
8
+ * `[event: KeyboardEvent]`, written as the parameter list a handler takes (#1436). Only
9
+ * when the outer brackets enclose the whole of it: `[a: A] | [b: B]` is left as it is.
10
+ */
11
+ export declare function asParameters(type: string): string;
12
+ export declare function toComponentDoc(meta: ComponentMeta, options?: VueDocgenOptions, eventDocs?: Map<string, EventDoc>, slotProps?: (slot: SlotMeta) => DocSlot['props'] & {}): ComponentDoc;
@@ -1,20 +1,12 @@
1
- const HIDDEN_TAGS = ['internal', 'private'];
2
- // Aliases: bits-ui mixes them about 50/50, reka-ui and nuxt/ui write `@defaultValue`.
3
- const DEFAULT_TAGS = ['default', 'defaultValue'];
1
+ import { declaredOnlyInPackages, DEFAULT_TAGS, HIDDEN_TAGS, normalizeDefault } from 'poveste/docgen';
4
2
  function tagsOf(tags) {
5
3
  return tags.map(({ name, text }) => text === undefined ? { name } : { name, text });
6
4
  }
7
5
  function isHidden(tags) {
8
6
  return tags.some(tag => HIDDEN_TAGS.includes(tag.name));
9
7
  }
10
- /** Declared only in installed packages none of which is allowed: an inherited attribute. */
11
8
  function isInherited(prop, allow) {
12
- const files = prop.getDeclarations().map(declaration => declaration.file.replaceAll('\\', '/'));
13
- return files.length > 0 && files.every(file => file.includes('/node_modules/') && !allow.some(pkg => file.includes(`/node_modules/${pkg}/`)));
14
- }
15
- /** A default as written, without the quoting a tag or the printer added. */
16
- export function normalizeDefault(value) {
17
- return value.trim().replace(/^`(.*)`$/s, '$1').replace(/^'(.*)'$/s, '"$1"');
9
+ return declaredOnlyInPackages(prop.getDeclarations().map(declaration => declaration.file), allow);
18
10
  }
19
11
  function toProp(prop) {
20
12
  const defaultTag = prop.tags.find(tag => DEFAULT_TAGS.includes(tag.name))?.text;
@@ -40,11 +32,45 @@ function toMember(member) {
40
32
  tags: tagsOf(member.tags),
41
33
  };
42
34
  }
43
- export function toComponentDoc(meta, options = {}) {
35
+ /**
36
+ * An event's payload, which vue-component-meta prints as the tuple it is declared as,
37
+ * `[event: KeyboardEvent]`, written as the parameter list a handler takes (#1436). Only
38
+ * when the outer brackets enclose the whole of it: `[a: A] | [b: B]` is left as it is.
39
+ */
40
+ export function asParameters(type) {
41
+ if (!type.startsWith('[')) {
42
+ return type;
43
+ }
44
+ let depth = 0;
45
+ for (let i = 0; i < type.length; i++) {
46
+ if ('[(<{'.includes(type[i])) {
47
+ depth++;
48
+ }
49
+ // The `>` of an arrow, `() => void`, closes nothing.
50
+ else if (']>)}'.includes(type[i]) && !(type[i] === '>' && type[i - 1] === '=') && --depth === 0) {
51
+ return i === type.length - 1 ? `(${type.slice(1, -1)})` : type;
52
+ }
53
+ }
54
+ return type;
55
+ }
56
+ // What vue-component-meta returns for an event is empty wherever its own reading is (#1462).
57
+ function withDocs(event, doc) {
58
+ return {
59
+ name: event.name,
60
+ description: event.description || doc?.description || '',
61
+ type: asParameters(event.type),
62
+ tags: event.tags.length ? event.tags : doc?.tags ?? [],
63
+ };
64
+ }
65
+ function toSlot(slot, slotProps) {
66
+ const props = slotProps(slot);
67
+ return { ...toMember(slot), ...props.length ? { props } : {} };
68
+ }
69
+ export function toComponentDoc(meta, options = {}, eventDocs = new Map(), slotProps = () => []) {
44
70
  const allow = options.allow ?? [];
45
71
  return {
46
72
  props: meta.props.filter(prop => !prop.global && !isHidden(prop.tags) && !isInherited(prop, allow)).map(toProp),
47
- slots: meta.slots.filter(slot => !isHidden(slot.tags)).map(toMember),
48
- events: meta.events.filter(event => !isHidden(event.tags)).map(toMember),
73
+ slots: meta.slots.filter(slot => !isHidden(slot.tags)).map(slot => toSlot(slot, slotProps)),
74
+ events: meta.events.map(event => withDocs(event, eventDocs.get(event.name))).filter(event => !isHidden(event.tags)).map(toMember),
49
75
  };
50
76
  }
@@ -0,0 +1,6 @@
1
+ import type { DocSlot } from '@poveste/shared';
2
+ import type ts from 'typescript';
3
+ import type { SlotMeta } from 'vue-component-meta';
4
+ type DocSlotProp = NonNullable<DocSlot['props']>[number];
5
+ export declare function readSlotProps(typescript: typeof ts, program: ts.Program | undefined, slot: SlotMeta): DocSlotProp[];
6
+ export {};
@@ -0,0 +1,21 @@
1
+ export function readSlotProps(typescript, program, slot) {
2
+ if (!program) {
3
+ return [];
4
+ }
5
+ const checker = program.getTypeChecker();
6
+ let type;
7
+ try {
8
+ type = slot.getTypeObject();
9
+ }
10
+ catch {
11
+ return [];
12
+ }
13
+ return type.getProperties().map((property) => {
14
+ const description = typescript.displayPartsToString(property.getDocumentationComment(checker));
15
+ return {
16
+ name: property.name,
17
+ type: checker.typeToString(checker.getTypeOfSymbol(property)),
18
+ ...description ? { description } : {},
19
+ };
20
+ });
21
+ }
@@ -1,3 +1,5 @@
1
1
  import type { Plugin } from 'poveste';
2
2
  export declare function HstVue(): Plugin;
3
3
  export * from './helpers.js';
4
+ export { povesteTesting } from './testing/vite.js';
5
+ export type { PovesteTestingOptions } from './testing/vite.js';
@@ -1,13 +1,13 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import { createRequire } from 'node:module';
3
- import { join } from 'node:path';
4
2
  import { fileURLToPath } from 'node:url';
3
+ import { CLIENT_DEPENDENCIES } from './client-dependencies.js';
5
4
  import generateStoryCommand from './commands/generate-story.server.js';
6
- import { typescriptProblem } from './docgen/typescript.js';
7
5
  import { VUE_SETUP_HOOK_NAMES } from './setup-hooks.js';
8
6
  import { listComponentFiles } from './util/list-components.js';
7
+ import { projectVueVersion } from './util/project-vue.js';
9
8
  import { VAPOR_INTEROP_ID, vaporInteropModule, withoutVaporPlugin } from './util/vapor.js';
10
9
  import { requireVuePlugin } from './util/vue-plugin.js';
10
+ const SCRIPT_COMPONENT = /(?<!\.d)\.[cm]?[jt]sx?$/;
11
11
  export function HstVue() {
12
12
  return {
13
13
  name: '@poveste/plugin-vue',
@@ -25,6 +25,14 @@ export function HstVue() {
25
25
  config() {
26
26
  return {
27
27
  vite: {
28
+ // The chrome's client code imports these, and the scan never reaches them: it
29
+ // loads through virtual modules, so a cold `poveste dev` found them only when the
30
+ // page asked, re-bundled and reloaded a Nuxt reader's first page (#1477). Named
31
+ // through this package, which every Vue, Nuxt and Quasar book lists, because the
32
+ // book itself cannot resolve them under pnpm.
33
+ optimizeDeps: {
34
+ include: CLIENT_DEPENDENCIES.map(name => `@poveste/plugin-vue > ${name}`),
35
+ },
28
36
  plugins: [
29
37
  requireVuePlugin(),
30
38
  withoutVaporPlugin(),
@@ -56,11 +64,14 @@ export function HstVue() {
56
64
  },
57
65
  };
58
66
  },
59
- // A story file documents nothing: it names the components that do.
67
+ // A story file documents nothing: it names the components that do. A script file is
68
+ // offered too, for a component written with `defineComponent` (#1463); the extractor
69
+ // answers nothing for one that isn't.
60
70
  docgen: {
61
- match: file => file.endsWith('.vue') && !file.endsWith('.story.vue'),
71
+ match: file => (file.endsWith('.vue') || SCRIPT_COMPONENT.test(file)) && !/\.story\.[^/]+$/.test(file) && !file.includes('/node_modules/'),
62
72
  module: fileURLToPath(new URL('./docgen/extractor.js', import.meta.url)),
63
- unavailable: () => typescriptProblem(),
73
+ // `vue-component-meta` requires `typescript` from where it is installed, an optional peer npm does not add.
74
+ typescript: createRequire(import.meta.url).resolve('vue-component-meta'),
64
75
  },
65
76
  supportPlugin: {
66
77
  id: 'vue3',
@@ -82,13 +93,5 @@ export function HstVue() {
82
93
  },
83
94
  };
84
95
  }
85
- function projectVueVersion(root) {
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
96
  export * from './helpers.js';
97
+ export { povesteTesting } from './testing/vite.js';
@@ -0,0 +1,28 @@
1
+ import type { Component } from 'vue';
2
+ export interface TestStoriesOptions {
3
+ /** The outer `describe`'s name. The story file's name by default. */
4
+ name?: string;
5
+ /**
6
+ * How long a variant's setup may take to settle, in milliseconds: an
7
+ * `initState` or an async `setup()` still pending then fails the test.
8
+ */
9
+ timeout?: number;
10
+ }
11
+ type StoryComponent = Component & {
12
+ __file?: string;
13
+ doc?: string;
14
+ };
15
+ /**
16
+ * Turns a story file into one test per variant. Each test renders the variant
17
+ * the way the preview does, and fails on what the preview would only log: an
18
+ * error from setup or render, a rejected `initState`, or a `[Vue warn]`.
19
+ *
20
+ * ```ts
21
+ * import { testStories } from '@poveste/plugin-vue/testing'
22
+ * import BaseButton from './BaseButton.story.vue'
23
+ *
24
+ * testStories(BaseButton)
25
+ * ```
26
+ */
27
+ export declare function testStories(component: StoryComponent, options?: TestStoriesOptions): void;
28
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface PovesteTestingOptions {
3
+ /** The book's poveste config file. Found from the Vite root by default. */
4
+ configFile?: string;
5
+ }
6
+ /**
7
+ * What a story needs in the user's own Vitest to render as `poveste dev` renders
8
+ * it: the book's setup file and the modules Poveste's client imports, with
9
+ * Poveste's own packages inlined so Vitest can serve those, and `vue` and the
10
+ * setup file pre-bundled for browser mode. Nothing else: no alias and no change
11
+ * to how anything resolves, so the test sees the project's Vite config as the
12
+ * project wrote it.
13
+ *
14
+ * Setup code that a plugin generates for `poveste dev`, such as the Nuxt app
15
+ * `@poveste/plugin-nuxt` boots, is left to the test environment, which already
16
+ * provides it: `@nuxt/test-utils`'s `nuxt` environment does in a Nuxt project.
17
+ */
18
+ export declare function povesteTesting(options?: PovesteTestingOptions): Plugin;
@@ -0,0 +1,171 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join, relative, resolve, sep } from 'node:path';
4
+ import { clientConfig, VIRTUAL_IDS } from '@poveste/shared';
5
+ import { getDefaultConfig, loadConfigFile, mergeConfig, resolveConfigFile } from 'poveste';
6
+ import { VUE_SETUP_HOOK_NAMES } from '../setup-hooks.js';
7
+ import { projectVueVersion } from '../util/project-vue.js';
8
+ import { VAPOR_INTEROP_ID, vaporInteropModule } from '../util/vapor.js';
9
+ const EMPTY_SETUP_ID = '\0poveste-testing:empty-setup';
10
+ const CONFIG_RESOLVED_ID = `\0${VIRTUAL_IDS.config}`;
11
+ const ENVIRONMENT_ID = 'virtual:$poveste-testing-environment';
12
+ // Vitest hands what it finds in `node_modules` to Node, which can't load the
13
+ // `virtual:` modules these packages import, so in a book that installs them no test
14
+ // ran (#1503). The workspace links them, and Vitest inlines a linked package.
15
+ const POVESTE_PACKAGES = [/\/node_modules\/poveste\//, /\/node_modules\/@poveste\//];
16
+ const EMPTY_SETUP = VUE_SETUP_HOOK_NAMES.map(name => `export const ${name} = undefined`).join('\n');
17
+ // `logEvent` from `poveste/client` reaches `@poveste/app`, which names these. A
18
+ // test never runs the code that reads them, but Vite's dependency scan follows
19
+ // the imports, and one it can't resolve skips pre-bundling and reloads the
20
+ // browser run partway through.
21
+ const INERT = {
22
+ [VIRTUAL_IDS.markdownFiles]: 'export const markdownFiles = []\n',
23
+ [VIRTUAL_IDS.componentDocs]: 'export const componentDocs = {}\n',
24
+ [VIRTUAL_IDS.supportPluginsClient]: 'export const clientSupportPlugins = {}\n',
25
+ };
26
+ // Nuxt boots in `@nuxt/test-utils`' `beforeAll`, after Vitest has collected the
27
+ // tests, and a story's own setup can call a Nuxt composable while it is being
28
+ // collected. So its own `setupNuxt` runs before collection, into the promise that
29
+ // `beforeAll` awaits, and Nuxt boots once and in full. Both names are internal to
30
+ // `@nuxt/test-utils`: if a version renames them, the story tests fail loudly.
31
+ function nuxtEnvironment(setupNuxt) {
32
+ return `import { tryUseNuxtApp } from '#app/nuxt'
33
+ import { setupNuxt } from ${JSON.stringify(setupNuxt)}
34
+
35
+ export async function environmentReady() {
36
+ const win = window
37
+ if (!win.__NUXT_VITEST_ENVIRONMENT__ || tryUseNuxtApp()) {
38
+ return
39
+ }
40
+ win.__NUXT_VITEST_ENVIRONMENT_PROMISE__ ??= setupNuxt()
41
+ await win.__NUXT_VITEST_ENVIRONMENT_PROMISE__
42
+ }
43
+
44
+ export function environmentApp() {
45
+ return tryUseNuxtApp()?.vueApp
46
+ }
47
+ `;
48
+ }
49
+ const NO_ENVIRONMENT = `export async function environmentReady() {}\nexport function environmentApp() {\n return undefined\n}\n`;
50
+ // `@nuxt/test-utils`' own boot, by file: its `exports` don't list it.
51
+ function setupNuxtFile(root) {
52
+ try {
53
+ const file = join(dirname(createRequire(join(root, 'package.json')).resolve('@nuxt/test-utils/package.json')), 'dist/runtime/shared/nuxt.mjs');
54
+ return existsSync(file) ? file : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ // Root-relative, as `poveste dev` reads it: the docs write `/src/poveste.setup.ts`,
61
+ // which `resolve` takes as absolute, so pre-bundling scanned a file outside the
62
+ // book and the setup file's imports were found late, with a reload (#1507).
63
+ function setupFileOf(config) {
64
+ const setupFile = config.setupFile;
65
+ const file = typeof setupFile === 'string' ? setupFile : setupFile && 'browser' in setupFile ? setupFile.browser : undefined;
66
+ return file?.replace(/^\/+/, '');
67
+ }
68
+ /**
69
+ * What a story needs in the user's own Vitest to render as `poveste dev` renders
70
+ * it: the book's setup file and the modules Poveste's client imports, with
71
+ * Poveste's own packages inlined so Vitest can serve those, and `vue` and the
72
+ * setup file pre-bundled for browser mode. Nothing else: no alias and no change
73
+ * to how anything resolves, so the test sees the project's Vite config as the
74
+ * project wrote it.
75
+ *
76
+ * Setup code that a plugin generates for `poveste dev`, such as the Nuxt app
77
+ * `@poveste/plugin-nuxt` boots, is left to the test environment, which already
78
+ * provides it: `@nuxt/test-utils`'s `nuxt` environment does in a Nuxt project.
79
+ */
80
+ export function povesteTesting(options = {}) {
81
+ let root = process.cwd();
82
+ let config = {};
83
+ return {
84
+ name: 'poveste:testing',
85
+ async config(user) {
86
+ root = resolve(user.root ?? process.cwd());
87
+ const file = options.configFile ? resolve(root, options.configFile) : resolveConfigFile(root);
88
+ const fromFile = file ? await loadConfigFile(file) : {};
89
+ // As `poveste dev` reads it: the config file, then the Vite config's key.
90
+ const fromVite = (user.poveste
91
+ ?? user.histoire ?? {});
92
+ config = mergeConfig(fromFile, fromVite);
93
+ // In browser mode Vite pre-bundles what its scan finds. It never sees `vue`,
94
+ // which compiled templates and this package's own bundle import, nor the
95
+ // setup file, which arrives as a virtual module. Each was bundled later,
96
+ // with a reload partway through the first run, and two copies of Vue
97
+ // failed it (#1503).
98
+ const setupFile = setupFileOf(config);
99
+ const optimizeDeps = {
100
+ include: ['vue'],
101
+ ...setupFile ? { entries: [relative(root, resolve(root, setupFile)).split(sep).join('/')] } : {},
102
+ };
103
+ // `true` already inlines everything, and an array merged into it would not.
104
+ if (user.test?.server?.deps?.inline === true) {
105
+ return { optimizeDeps };
106
+ }
107
+ return { optimizeDeps, test: { server: { deps: { inline: POVESTE_PACKAGES } } } };
108
+ },
109
+ configResolved(resolved) {
110
+ root = resolved.root;
111
+ },
112
+ async resolveId(id, importer) {
113
+ if (id === VIRTUAL_IDS.setup) {
114
+ const file = setupFileOf(config);
115
+ if (file) {
116
+ return this.resolve(resolve(root, file), importer, { skipSelf: true });
117
+ }
118
+ return EMPTY_SETUP_ID;
119
+ }
120
+ if (id === VIRTUAL_IDS.generatedGlobalSetup) {
121
+ return EMPTY_SETUP_ID;
122
+ }
123
+ if (id === VIRTUAL_IDS.config) {
124
+ return CONFIG_RESOLVED_ID;
125
+ }
126
+ if (id === VAPOR_INTEROP_ID) {
127
+ return `\0${VAPOR_INTEROP_ID}`;
128
+ }
129
+ if (Object.hasOwn(INERT, id)) {
130
+ return `\0${id}`;
131
+ }
132
+ if (id === ENVIRONMENT_ID) {
133
+ return `\0${ENVIRONMENT_ID}`;
134
+ }
135
+ return undefined;
136
+ },
137
+ // A story's `<docs>` block, which `poveste dev` renders as markdown. A test
138
+ // renders the variants, not the docs, so it only has to import.
139
+ transform(_code, id) {
140
+ if (id.includes('?vue&type=docs')) {
141
+ return 'export default () => {}';
142
+ }
143
+ return undefined;
144
+ },
145
+ async load(id) {
146
+ if (id === EMPTY_SETUP_ID) {
147
+ return EMPTY_SETUP;
148
+ }
149
+ if (id === CONFIG_RESOLVED_ID) {
150
+ return `export const config = ${JSON.stringify(clientConfig({ ...getDefaultConfig(), ...config }))}\nexport const logos = {}\n`;
151
+ }
152
+ if (id === `\0${VAPOR_INTEROP_ID}`) {
153
+ return vaporInteropModule(projectVueVersion(root));
154
+ }
155
+ if (id.startsWith('\0') && Object.hasOwn(INERT, id.slice(1))) {
156
+ return INERT[id.slice(1)];
157
+ }
158
+ // The app the test environment boots: Nuxt's, under `@nuxt/test-utils`' `nuxt`
159
+ // environment. `poveste dev` renders a story inside the Nuxt app its plugin
160
+ // boots, and a test does the same. Told by Nuxt's own `#app` alias and the
161
+ // test utils being installed, not by `#imports`, which unimport also serves.
162
+ if (id === `\0${ENVIRONMENT_ID}`) {
163
+ const setupNuxt = setupNuxtFile(root);
164
+ return setupNuxt && await this.resolve('#app/nuxt', resolve(root, 'index.html'))
165
+ ? nuxtEnvironment(setupNuxt)
166
+ : NO_ENVIRONMENT;
167
+ }
168
+ return undefined;
169
+ },
170
+ };
171
+ }
@@ -0,0 +1 @@
1
+ export declare function projectVueVersion(root: string): string | undefined;