@poveste/plugin-vue 0.19.1 → 0.20.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.
@@ -1,8 +1,9 @@
1
- import { createPreviewHost as e } from "./host.js";
2
- import { defineComponent as t, h as n, onMounted as r, onUnmounted as i, ref as a, watch as o } from "@poveste/vendors/vue";
3
- import { reactive as s } from "vue";
1
+ import { warnIfForeignVue as e } from "./util.js";
2
+ import { createPreviewHost as t } from "./host.js";
3
+ import { defineComponent as n, h as r, onMounted as i, onUnmounted as a, ref as o, watch as s } from "@poveste/vendors/vue";
4
+ import { reactive as c } from "vue";
4
5
  //#region src/client/app/MountStory.ts
5
- var c = t({
6
+ var l = n({
6
7
  name: "MountStory",
7
8
  props: {
8
9
  story: {
@@ -18,43 +19,44 @@ var c = t({
18
19
  default: !0
19
20
  }
20
21
  },
21
- setup(t) {
22
- let n = a(), c = s({
22
+ setup(n) {
23
+ e("MountStory");
24
+ let r = o(), l = c({
23
25
  mode: "mount",
24
26
  slotName: "default",
25
27
  currentVariant: null,
26
- targetVariantId: t.targetVariantId,
27
- syncState: t.syncState,
28
+ targetVariantId: n.targetVariantId,
29
+ syncState: n.syncState,
28
30
  externalState: null,
29
31
  nextVariantIndex: { value: 0 }
30
- }), l = null;
31
- async function u() {
32
- n.value && (l = e({
32
+ }), u = null;
33
+ async function d() {
34
+ r.value && (u = t({
33
35
  name: "MountStorySubApp",
34
- el: n.value,
35
- getStory: () => t.story,
36
+ el: r.value,
37
+ getStory: () => n.story,
36
38
  getVariant: () => null,
37
- renderContext: c
38
- }), await l.mount());
39
+ renderContext: l
40
+ }), await u.mount());
39
41
  }
40
- function d() {
41
- l?.unmount(), l = null;
42
+ function f() {
43
+ u?.unmount(), u = null;
42
44
  }
43
- return o(() => t.story.id, async () => {
44
- d(), await u();
45
- }), r(async () => {
46
- await u();
47
- }), o(() => t.story.variants, () => {
48
- l?.forceUpdate();
49
- }), o(() => t.targetVariantId, (e) => {
50
- c.targetVariantId = e;
51
- }), i(() => {
52
- d();
53
- }), { el: n };
45
+ return s(() => n.story.id, async () => {
46
+ f(), await d();
47
+ }), i(async () => {
48
+ await d();
49
+ }), s(() => n.story.variants, () => {
50
+ u?.forceUpdate();
51
+ }), s(() => n.targetVariantId, (e) => {
52
+ l.targetVariantId = e;
53
+ }), a(() => {
54
+ f();
55
+ }), { el: r };
54
56
  },
55
57
  render() {
56
- return n("div", { ref: "el" });
58
+ return r("div", { ref: "el" });
57
59
  }
58
60
  });
59
61
  //#endregion
60
- export { c as default };
62
+ export { l as default };
@@ -1,9 +1,9 @@
1
- import { syncStateBundledAndExternal as e } from "./util.js";
2
- import { createPreviewHost as t } from "./host.js";
3
- import { defineComponent as n, h as r, onBeforeUnmount as i, onMounted as a, ref as o, watch as s } from "@poveste/vendors/vue";
4
- import { reactive as c } from "vue";
1
+ import { syncStateBundledAndExternal as e, warnIfForeignVue as t } from "./util.js";
2
+ import { createPreviewHost as n } from "./host.js";
3
+ import { defineComponent as r, h as i, onBeforeUnmount as a, onMounted as o, ref as s, watch as c } from "@poveste/vendors/vue";
4
+ import { reactive as l } from "vue";
5
5
  //#region src/client/app/RenderStory.ts
6
- var l = n({
6
+ var u = r({
7
7
  name: "RenderStory",
8
8
  props: {
9
9
  variant: {
@@ -20,54 +20,49 @@ var l = n({
20
20
  }
21
21
  },
22
22
  emits: { ready: () => !0 },
23
- setup(n, { emit: r }) {
24
- let l = o(), u = null, d = !1, f = c({}), p = e(n.variant.state, f), m = c({
23
+ setup(r, { emit: i }) {
24
+ t("RenderStory");
25
+ let u = s(), d = null, f = !1, p = l({}), m = e(r.variant.state, p), h = l({
25
26
  mode: "render",
26
- slotName: n.slotName,
27
- currentVariant: n.variant,
28
- externalState: f,
27
+ slotName: r.slotName,
28
+ currentVariant: r.variant,
29
+ externalState: p,
29
30
  nextVariantIndex: { value: 0 }
30
31
  });
31
- function h() {
32
- u?.unmount(), u = null;
33
- }
34
32
  function g() {
35
- p?.stop(), p = e(n.variant.state, f);
33
+ d?.unmount(), d = null;
34
+ }
35
+ function _() {
36
+ m?.stop(), m = e(r.variant.state, p);
36
37
  }
37
- async function _() {
38
- if (!d) {
39
- if (d = !0, h(), m.currentVariant = n.variant, m.slotName = n.slotName, !l.value) {
40
- d = !1;
41
- return;
42
- }
43
- u = t({
44
- name: "RenderStorySubApp",
45
- el: l.value,
46
- getStory: () => n.story,
47
- getVariant: () => n.variant,
48
- renderContext: m,
49
- wrapInDiv: !0
50
- }), await u.mount(), d = !1, r("ready");
51
- }
38
+ async function v() {
39
+ f || (f = !0, g(), h.currentVariant = r.variant, h.slotName = r.slotName, u.value ? (d = n({
40
+ name: "RenderStorySubApp",
41
+ el: u.value,
42
+ getStory: () => r.story,
43
+ getVariant: () => r.variant,
44
+ renderContext: h,
45
+ wrapInDiv: !0
46
+ }), await d.mount(), f = !1, i("ready")) : f = !1);
52
47
  }
53
- return a(async () => {
54
- n.variant.configReady && await _();
55
- }), s(() => n.variant, () => {
56
- m.currentVariant = n.variant, g();
57
- }), s(() => [
58
- n.story.id,
59
- n.slotName,
60
- n.variant.id,
61
- n.variant.configReady
48
+ return o(async () => {
49
+ r.variant.configReady && await v();
50
+ }), c(() => r.variant, () => {
51
+ h.currentVariant = r.variant, _();
52
+ }), c(() => [
53
+ r.story.id,
54
+ r.slotName,
55
+ r.variant.id,
56
+ r.variant.configReady
62
57
  ], async ([, e]) => {
63
- m.currentVariant = n.variant, m.slotName = e, n.variant.configReady && !d && (u ? u.forceUpdate() : await _());
64
- }), i(() => {
65
- p?.stop(), h();
66
- }), { sandbox: l };
58
+ h.currentVariant = r.variant, h.slotName = e, r.variant.configReady && !f && (d ? d.forceUpdate() : await v());
59
+ }), a(() => {
60
+ m?.stop(), g();
61
+ }), { sandbox: u };
67
62
  },
68
63
  render() {
69
- return r("div", { ref: "sandbox" });
64
+ return i("div", { ref: "sandbox" });
70
65
  }
71
66
  });
72
67
  //#endregion
73
- export { l as default };
68
+ export { u as default };
@@ -69,16 +69,12 @@ var m = c({
69
69
  function x() {
70
70
  let e = 0, t = m?.targetVariantId, n = t ? l.story.variants.findIndex((e) => e.id === t) : -1, r = (t) => {
71
71
  let i = [];
72
- for (let s of t) {
73
- if (s.type?.__povesteType === "variant") {
74
- let t = l.story.variants[e];
75
- if (e++, !t || n !== -1 && e - 1 !== n) continue;
76
- let r = { variant: t };
77
- s.key ?? (r.key = t.id), !s.props?.initState && !s.props?.["init-state"] && (r.initState = a.initState), Object.assign(r, b(s)), i.push(o(s, r));
78
- continue;
79
- }
80
- Array.isArray(s.children) && (s.children = r(s.children)), i.push(s);
81
- }
72
+ for (let s of t) if (s.type?.__povesteType === "variant") {
73
+ let t = l.story.variants[e];
74
+ if (e++, !t || n !== -1 && e - 1 !== n) continue;
75
+ let r = { variant: t };
76
+ s.key ?? (r.key = t.id), !s.props?.initState && !s.props?.["init-state"] && (r.initState = a.initState), Object.assign(r, b(s)), i.push(o(s, r));
77
+ } else Array.isArray(s.children) && (s.children = r(s.children)), i.push(s);
82
78
  return i;
83
79
  };
84
80
  return r(c.slots.default?.() ?? []);
@@ -98,13 +94,7 @@ var m = c({
98
94
  if (t !== -1) {
99
95
  let r = 0, i = !1, a = (e) => {
100
96
  let n = [];
101
- for (let s of e) {
102
- if (s?.type?.__povesteType === "variant") {
103
- r === t && (n.push(o(s, b(s))), i = !0), r++;
104
- continue;
105
- }
106
- Array.isArray(s?.children) && s.children.length && (s.children = a(s.children)), n.push(s);
107
- }
97
+ for (let s of e) s?.type?.__povesteType === "variant" ? (r === t && (n.push(o(s, b(s))), i = !0), r++) : (Array.isArray(s?.children) && s.children.length && (s.children = a(s.children)), n.push(s));
108
98
  return n;
109
99
  }, s = a(n);
110
100
  if (i) return e.nextVariantIndex.value = t, s;
@@ -1,66 +1,66 @@
1
- import { isRef as e, unref as t, watch as n } from "@poveste/vendors/vue";
2
- import { getCurrentInstance as r, isReadonly as i, isRef as a, markRaw as o, unref as s, watch as c } from "vue";
3
- import { applyState as l, createStateBaseline as u, isMarkedRaw as d, isPlainObject as f } from "@poveste/shared";
1
+ import { getCurrentInstance as e, isRef as t, unref as n, watch as r } from "@poveste/vendors/vue";
2
+ import { getCurrentInstance as i, isReadonly as a, isRef as o, markRaw as s, unref as c, watch as l } from "vue";
3
+ import { applyState as u, createStateBaseline as d, isMarkedRaw as f, isPlainObject as p } from "@poveste/shared";
4
4
  //#region src/client/app/util.ts
5
- var p = (e) => typeof e == "object" && !!e;
6
- function m(e, t = /* @__PURE__ */ new WeakMap()) {
7
- let n = a(e) ? s(e) : e;
5
+ var m = (e) => typeof e == "object" && !!e;
6
+ function h(e, t = /* @__PURE__ */ new WeakMap()) {
7
+ let n = o(e) ? c(e) : e;
8
8
  if (typeof n == "symbol") return n.toString();
9
- if (!p(n) || d(n)) return n;
9
+ if (!m(n) || f(n)) return n;
10
10
  if (t.has(n)) return t.get(n);
11
- if (!Array.isArray(n) && !f(n)) return o(n);
11
+ if (!Array.isArray(n) && !p(n)) return s(n);
12
12
  if (Array.isArray(n)) {
13
13
  let e = [];
14
- return t.set(n, e), e.push(...n.map((e) => m(e, t))), e;
14
+ return t.set(n, e), e.push(...n.map((e) => h(e, t))), e;
15
15
  }
16
16
  {
17
17
  let e = {};
18
- return t.set(n, e), h(n, e, t), e;
18
+ return t.set(n, e), g(n, e, t), e;
19
19
  }
20
20
  }
21
- function h(e, t, n = /* @__PURE__ */ new WeakMap()) {
21
+ function g(e, t, n = /* @__PURE__ */ new WeakMap()) {
22
22
  Object.keys(e).forEach((r) => {
23
- t[r] = m(e[r], n);
23
+ t[r] = h(e[r], n);
24
24
  });
25
25
  }
26
- function g(n, r = /* @__PURE__ */ new WeakMap()) {
27
- let i = e(n) ? t(n) : n;
26
+ function _(e, r = /* @__PURE__ */ new WeakMap()) {
27
+ let i = t(e) ? n(e) : e;
28
28
  if (typeof i == "symbol") return i.toString();
29
- if (!p(i) || d(i)) return i;
29
+ if (!m(i) || f(i)) return i;
30
30
  if (r.has(i)) return r.get(i);
31
- if (!Array.isArray(i) && !f(i)) return o(i);
31
+ if (!Array.isArray(i) && !p(i)) return s(i);
32
32
  if (Array.isArray(i)) {
33
33
  let e = [];
34
- return r.set(i, e), e.push(...i.map((e) => g(e, r))), e;
34
+ return r.set(i, e), e.push(...i.map((e) => _(e, r))), e;
35
35
  }
36
36
  {
37
37
  let e = {};
38
- return r.set(i, e), _(i, e, r), e;
38
+ return r.set(i, e), v(i, e, r), e;
39
39
  }
40
40
  }
41
- function _(e, t, n = /* @__PURE__ */ new WeakMap()) {
41
+ function v(e, t, n = /* @__PURE__ */ new WeakMap()) {
42
42
  Object.keys(e).forEach((r) => {
43
- t[r] = g(e[r], n);
43
+ t[r] = _(e[r], n);
44
44
  });
45
45
  }
46
- function v(e, t) {
46
+ function y(e, t) {
47
47
  if (!t.length) return e;
48
48
  let n = { ...e };
49
49
  for (let e of t) delete n[e];
50
50
  return n;
51
51
  }
52
- function y(e, t, r = []) {
53
- let i = u(), a = n(() => e, (e) => {
52
+ function b(e, t, n = []) {
53
+ let i = d(), a = r(() => e, (e) => {
54
54
  if (e == null) return;
55
- let n = i.take(v(g(e), r));
56
- n && l(t, n);
55
+ let r = i.take(y(_(e), n));
56
+ r && u(t, r);
57
57
  }, {
58
58
  deep: !0,
59
59
  immediate: !0
60
- }), o = c(() => t, (t) => {
60
+ }), o = l(() => t, (t) => {
61
61
  if (t == null) return;
62
- let n = i.take(v(m(t), r));
63
- n && l(e, n);
62
+ let r = i.take(y(h(t), n));
63
+ r && u(e, r);
64
64
  }, {
65
65
  deep: !0,
66
66
  immediate: !0
@@ -69,13 +69,17 @@ function y(e, t, r = []) {
69
69
  a(), o();
70
70
  } };
71
71
  }
72
- function b(e) {
73
- return !(a(e) && i(e));
74
- }
75
72
  function x(e) {
76
- let t = r();
73
+ return !(o(e) && a(e));
74
+ }
75
+ function S(e) {
76
+ let t = i();
77
77
  if (!t) throw Error(`[poveste] <${e}> can only be set up inside a component`);
78
78
  return t;
79
79
  }
80
+ var C = !1;
81
+ function w(t) {
82
+ C || e() || (C = !0, console.warn(`[poveste] ${t} 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\`.`));
83
+ }
80
84
  //#endregion
81
- export { g as _toRawDeep, b as isWritableBinding, y as syncStateBundledAndExternal, m as toRawDeep, x as useInstance };
85
+ export { _ as _toRawDeep, x as isWritableBinding, b as syncStateBundledAndExternal, h as toRawDeep, S as useInstance, w as warnIfForeignVue };
@@ -1,2 +1 @@
1
- import { run as e } from "./server/run.js";
2
- export { e as run };
1
+ export { run } from "./server/run.js";
@@ -50,3 +50,15 @@ export declare function syncStateBundledAndExternal(bundledState: Record<string,
50
50
  */
51
51
  export declare function isWritableBinding(value: unknown): boolean;
52
52
  export declare function useInstance(name: string): ComponentInternalInstance;
53
+ /**
54
+ * Warns, once, when a component built against `@poveste/vendors/vue` is
55
+ * rendered by another copy of Vue.
56
+ *
57
+ * The other copy runs the component's `setup`, so the vendored one has no
58
+ * current instance and drops every hook: the story renders nothing, and no build
59
+ * of Vue says why. A test mounting `MountStory` with `createApp` from `vue` read
60
+ * it as a story with no variants (#940).
61
+ */
62
+ export declare function warnIfForeignVue(component: string): void;
63
+ /** For specs: lets the warning be seen again. */
64
+ export declare function resetForeignVueWarning(): void;
@@ -0,0 +1,8 @@
1
+ import type { DocgenExtractor, DocgenExtractorContext } from '@poveste/shared';
2
+ export declare function findTsconfig(root: string): string | undefined;
3
+ /**
4
+ * Runs in the docgen worker. `vue-component-meta` is imported here rather than at
5
+ * the top, so a book without TypeScript fails this one request, answered with its
6
+ * error, and keeps its runtime props.
7
+ */
8
+ export declare function createExtractor(context: DocgenExtractorContext): Promise<DocgenExtractor>;
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'pathe';
3
+ 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
+ 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
+ /**
12
+ * Runs in the docgen worker. `vue-component-meta` is imported here rather than at
13
+ * the top, so a book without TypeScript fails this one request, answered with its
14
+ * error, and keeps its runtime props.
15
+ */
16
+ export async function createExtractor(context) {
17
+ const { createChecker, createCheckerByJson } = await import('vue-component-meta');
18
+ const options = (context.options ?? {});
19
+ const checkerOptions = { forceUseTs: true, printer: { newLine: 1 } };
20
+ const tsconfig = findTsconfig(context.root);
21
+ const checker = tsconfig
22
+ ? createChecker(tsconfig, checkerOptions)
23
+ : createCheckerByJson(context.root, { include: ['**/*'], exclude: ['node_modules'] }, checkerOptions);
24
+ return {
25
+ extract: file => toComponentDoc(checker.getComponentMeta(file), options),
26
+ update(file) {
27
+ if (!SOURCE.test(file)) {
28
+ return;
29
+ }
30
+ if (existsSync(file)) {
31
+ checker.updateFile(file, readFileSync(file, 'utf8'));
32
+ }
33
+ else {
34
+ checker.deleteFile(file);
35
+ }
36
+ },
37
+ sources: () => checker.getProgram()?.getSourceFiles().map(sourceFile => sourceFile.fileName).filter(file => !file.includes('/node_modules/')) ?? [],
38
+ // The program is released with the checker; this drops what it cached.
39
+ dispose: () => checker.clearCache(),
40
+ };
41
+ }
@@ -0,0 +1,9 @@
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;
@@ -0,0 +1,50 @@
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'];
4
+ function tagsOf(tags) {
5
+ return tags.map(({ name, text }) => text === undefined ? { name } : { name, text });
6
+ }
7
+ function isHidden(tags) {
8
+ return tags.some(tag => HIDDEN_TAGS.includes(tag.name));
9
+ }
10
+ /** Declared only in installed packages none of which is allowed: an inherited attribute. */
11
+ 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"');
18
+ }
19
+ function toProp(prop) {
20
+ const defaultTag = prop.tags.find(tag => DEFAULT_TAGS.includes(tag.name))?.text;
21
+ return {
22
+ name: prop.name,
23
+ ...prop.description ? { description: prop.description } : {},
24
+ type: prop.type,
25
+ required: prop.required,
26
+ ...prop.default !== undefined ? { default: prop.default } : {},
27
+ ...defaultTag !== undefined ? { defaultTag } : {},
28
+ // The code's default is what runs, so it is the one shown; the flag is for the reader.
29
+ ...prop.default !== undefined && defaultTag !== undefined && normalizeDefault(prop.default) !== normalizeDefault(defaultTag)
30
+ ? { defaultConflict: true }
31
+ : {},
32
+ tags: tagsOf(prop.tags),
33
+ };
34
+ }
35
+ function toMember(member) {
36
+ return {
37
+ name: member.name,
38
+ ...member.description ? { description: member.description } : {},
39
+ ...member.type ? { type: member.type } : {},
40
+ tags: tagsOf(member.tags),
41
+ };
42
+ }
43
+ export function toComponentDoc(meta, options = {}) {
44
+ const allow = options.allow ?? [];
45
+ return {
46
+ 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),
49
+ };
50
+ }
@@ -0,0 +1,9 @@
1
+ /** The version of the first `typescript` found in `dirs`, searched in order as Node searches `node_modules`. */
2
+ export declare function typescriptVersionIn(dirs: string[]): string | undefined;
3
+ /** Why auto-docs cannot read a Vue component's types with the TypeScript in `dirs`, or nothing when it can. */
4
+ export declare function typescriptProblemIn(dirs: string[]): string | undefined;
5
+ /**
6
+ * The same question for the TypeScript `vue-component-meta` will load: it requires
7
+ * `typescript` from where it is installed, an optional peer that npm does not add.
8
+ */
9
+ export declare function typescriptProblem(): string | undefined;
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'pathe';
4
+ // `vue-component-meta` 3.3 calls `ts.readJsonConfigFile`, which TypeScript 7 no
5
+ // longer has, and no release of it reads TypeScript 7 yet (#1382).
6
+ const NEWEST_MAJOR = 6;
7
+ const FIX = 'Install `typescript@6` in the book to turn it on, or set `autoDocs: false` to turn it off without this message.';
8
+ /** The version of the first `typescript` found in `dirs`, searched in order as Node searches `node_modules`. */
9
+ export function typescriptVersionIn(dirs) {
10
+ for (const dir of dirs) {
11
+ const manifest = join(dir, 'typescript', 'package.json');
12
+ if (existsSync(manifest)) {
13
+ return JSON.parse(readFileSync(manifest, 'utf8')).version;
14
+ }
15
+ }
16
+ return undefined;
17
+ }
18
+ /** Why auto-docs cannot read a Vue component's types with the TypeScript in `dirs`, or nothing when it can. */
19
+ export function typescriptProblemIn(dirs) {
20
+ const version = typescriptVersionIn(dirs);
21
+ if (!version) {
22
+ return `Auto-docs is off: it reads component types with TypeScript, which this book does not have. ${FIX}`;
23
+ }
24
+ if (Number.parseInt(version, 10) > NEWEST_MAJOR) {
25
+ return `Auto-docs is off: it reads component types with TypeScript ${NEWEST_MAJOR} or earlier, and this book has TypeScript ${version}. ${FIX}`;
26
+ }
27
+ return undefined;
28
+ }
29
+ /**
30
+ * The same question for the TypeScript `vue-component-meta` will load: it requires
31
+ * `typescript` from where it is installed, an optional peer that npm does not add.
32
+ */
33
+ export function typescriptProblem() {
34
+ const meta = createRequire(import.meta.url).resolve('vue-component-meta');
35
+ return typescriptProblemIn(createRequire(meta).resolve.paths('typescript') ?? []);
36
+ }
@@ -1,10 +1,13 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import generateStoryCommand from './commands/generate-story.server.js';
6
+ import { typescriptProblem } from './docgen/typescript.js';
5
7
  import { VUE_SETUP_HOOK_NAMES } from './setup-hooks.js';
6
8
  import { listComponentFiles } from './util/list-components.js';
7
9
  import { VAPOR_INTEROP_ID, vaporInteropModule, withoutVaporPlugin } from './util/vapor.js';
10
+ import { requireVuePlugin } from './util/vue-plugin.js';
8
11
  export function HstVue() {
9
12
  return {
10
13
  name: '@poveste/plugin-vue',
@@ -23,6 +26,7 @@ export function HstVue() {
23
26
  return {
24
27
  vite: {
25
28
  plugins: [
29
+ requireVuePlugin(),
26
30
  withoutVaporPlugin(),
27
31
  (() => {
28
32
  let root = process.cwd();
@@ -52,6 +56,12 @@ export function HstVue() {
52
56
  },
53
57
  };
54
58
  },
59
+ // A story file documents nothing: it names the components that do.
60
+ docgen: {
61
+ match: file => file.endsWith('.vue') && !file.endsWith('.story.vue'),
62
+ module: fileURLToPath(new URL('./docgen/extractor.js', import.meta.url)),
63
+ unavailable: () => typescriptProblem(),
64
+ },
55
65
  supportPlugin: {
56
66
  id: 'vue3',
57
67
  moduleName: '@poveste/plugin-vue',
@@ -0,0 +1,10 @@
1
+ import type { Plugin as VitePlugin } from 'vite';
2
+ /**
3
+ * What to say when a book's Vite config has no Vue plugin. Without one, Vite
4
+ * answers the first `.vue` file with "Install @vitejs/plugin-vue", which sends a
5
+ * Quasar reader to install a package their project does not need: Quasar brings
6
+ * its own Vue plugin, and `HstQuasar()` is what hands it to Poveste (#1337).
7
+ */
8
+ export declare function noVuePluginMessage(root: string, exists?: (path: string) => boolean): string;
9
+ /** Reports, once per process, a resolved Vite config with no Vue plugin in it. */
10
+ export declare function requireVuePlugin(): VitePlugin;
@@ -0,0 +1,28 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const QUASAR_CONFIGS = ['quasar.config.js', 'quasar.config.mjs', 'quasar.config.cjs', 'quasar.config.ts'];
4
+ /**
5
+ * What to say when a book's Vite config has no Vue plugin. Without one, Vite
6
+ * answers the first `.vue` file with "Install @vitejs/plugin-vue", which sends a
7
+ * Quasar reader to install a package their project does not need: Quasar brings
8
+ * its own Vue plugin, and `HstQuasar()` is what hands it to Poveste (#1337).
9
+ */
10
+ export function noVuePluginMessage(root, exists = existsSync) {
11
+ const fix = QUASAR_CONFIGS.some(name => exists(join(root, name)))
12
+ ? 'This is a Quasar project: add `HstQuasar()` from `@poveste/plugin-quasar` to the config\'s `plugins`, which brings Quasar\'s own Vue plugin.'
13
+ : 'Add `@vitejs/plugin-vue` to `vite.config`, or in a Nuxt project `HstNuxt()` from `@poveste/plugin-nuxt` to the config\'s `plugins`.';
14
+ return `[poveste] The Vite config has no Vue plugin, so no \`.vue\` story can be compiled. ${fix}`;
15
+ }
16
+ /** Reports, once per process, a resolved Vite config with no Vue plugin in it. */
17
+ export function requireVuePlugin() {
18
+ let reported = false;
19
+ return {
20
+ name: 'poveste-plugin-vue:require-vue-plugin',
21
+ configResolved(config) {
22
+ if (!reported && !config.plugins.some(plugin => plugin.name === 'vite:vue')) {
23
+ reported = true;
24
+ console.error(noVuePluginMessage(config.root));
25
+ }
26
+ },
27
+ };
28
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/plugin-vue",
3
3
  "type": "module",
4
- "version": "0.19.1",
4
+ "version": "0.20.0",
5
5
  "description": "Poveste plugin for Vue.js support",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan",
@@ -72,27 +72,28 @@
72
72
  "node": ">=22.22.2"
73
73
  },
74
74
  "peerDependencies": {
75
- "poveste": "^0.19.1",
75
+ "poveste": "^0.20.0",
76
76
  "vue": "^3.5.43"
77
77
  },
78
78
  "dependencies": {
79
- "@poveste/controls": "^0.19.1",
80
- "@poveste/shared": "^0.19.1",
81
- "@poveste/vendors": "^0.19.1",
79
+ "@poveste/controls": "^0.20.0",
80
+ "@poveste/shared": "^0.20.0",
81
+ "@poveste/vendors": "^0.20.0",
82
82
  "change-case": "^5.4.4",
83
83
  "ignore": "^7.0.10",
84
- "launch-editor": "^2.9.1",
84
+ "launch-editor": "^2.14.2",
85
85
  "pathe": "^2.0.3",
86
- "tinyglobby": "^0.2.17"
86
+ "tinyglobby": "^0.2.17",
87
+ "vue-component-meta": "^3.3.13"
87
88
  },
88
89
  "devDependencies": {
89
- "@types/node": "^26.6.3",
90
- "concurrently": "^10.0.5",
91
- "poveste": "0.19.1",
90
+ "@types/node": "^26.6.5",
91
+ "concurrently": "^10.0.6",
92
+ "poveste": "0.20.0",
92
93
  "typescript": "6.0.3",
93
- "vite": "^8.3.1",
94
- "vitest": "^5.0.2",
95
- "vue": "^3.5.43",
94
+ "vite": "^8.3.4",
95
+ "vitest": "^5.0.4",
96
+ "vue": "^3.5.44",
96
97
  "vue-vapor-rc": "npm:vue@3.6.0-rc.10"
97
98
  },
98
99
  "scripts": {
@@ -12,6 +12,7 @@ import {
12
12
  } from '@poveste/vendors/vue'
13
13
  import { reactive } from 'vue'
14
14
  import { createPreviewHost } from './host.js'
15
+ import { warnIfForeignVue } from './util.js'
15
16
 
16
17
  export default _defineComponent({
17
18
  name: 'MountStory',
@@ -34,6 +35,7 @@ export default _defineComponent({
34
35
  },
35
36
 
36
37
  setup(props) {
38
+ warnIfForeignVue('MountStory')
37
39
  const el = _ref<HTMLDivElement>()
38
40
  const renderContext = reactive({
39
41
  mode: 'mount' as const,
@@ -12,7 +12,7 @@ import {
12
12
  } from '@poveste/vendors/vue'
13
13
  import { reactive } from 'vue'
14
14
  import { createPreviewHost } from './host.js'
15
- import { syncStateBundledAndExternal } from './util.js'
15
+ import { syncStateBundledAndExternal, warnIfForeignVue } from './util.js'
16
16
 
17
17
  export default _defineComponent({
18
18
  name: 'RenderStory',
@@ -39,6 +39,7 @@ export default _defineComponent({
39
39
  },
40
40
 
41
41
  setup(props, { emit }) {
42
+ warnIfForeignVue('RenderStory')
42
43
  const sandbox = _ref<HTMLDivElement>()
43
44
  let host: ReturnType<typeof createPreviewHost> | null = null
44
45
  let mounting = false
@@ -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,
@@ -241,3 +242,27 @@ export function useInstance(name: string): ComponentInternalInstance {
241
242
  }
242
243
  return vm
243
244
  }
245
+
246
+ let warnedForeignVue = false
247
+
248
+ /**
249
+ * Warns, once, when a component built against `@poveste/vendors/vue` is
250
+ * rendered by another copy of Vue.
251
+ *
252
+ * The other copy runs the component's `setup`, so the vendored one has no
253
+ * current instance and drops every hook: the story renders nothing, and no build
254
+ * of Vue says why. A test mounting `MountStory` with `createApp` from `vue` read
255
+ * it as a story with no variants (#940).
256
+ */
257
+ export function warnIfForeignVue(component: string) {
258
+ if (warnedForeignVue || _getCurrentInstance()) {
259
+ return
260
+ }
261
+ warnedForeignVue = true
262
+ 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\`.`)
263
+ }
264
+
265
+ /** For specs: lets the warning be seen again. */
266
+ export function resetForeignVueWarning() {
267
+ warnedForeignVue = false
268
+ }
@@ -0,0 +1,47 @@
1
+ import type { DocgenExtractor, DocgenExtractorContext } from '@poveste/shared'
2
+ import type { VueDocgenOptions } from './meta.js'
3
+ import { existsSync, readFileSync } from 'node:fs'
4
+ import { join } from 'pathe'
5
+ import { toComponentDoc } from './meta.js'
6
+
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
+ const SOURCE = /\.(?:vue|[cm]?[jt]sx?)$/
11
+
12
+ export function findTsconfig(root: string) {
13
+ return TSCONFIGS.map(name => join(root, name)).find(file => existsSync(file))
14
+ }
15
+
16
+ /**
17
+ * Runs in the docgen worker. `vue-component-meta` is imported here rather than at
18
+ * the top, so a book without TypeScript fails this one request, answered with its
19
+ * error, and keeps its runtime props.
20
+ */
21
+ export async function createExtractor(context: DocgenExtractorContext): Promise<DocgenExtractor> {
22
+ const { createChecker, createCheckerByJson } = await import('vue-component-meta')
23
+ const options = (context.options ?? {}) as VueDocgenOptions
24
+ const checkerOptions = { forceUseTs: true, printer: { newLine: 1 } }
25
+ const tsconfig = findTsconfig(context.root)
26
+ const checker = tsconfig
27
+ ? createChecker(tsconfig, checkerOptions)
28
+ : createCheckerByJson(context.root, { include: ['**/*'], exclude: ['node_modules'] }, checkerOptions)
29
+
30
+ return {
31
+ extract: file => toComponentDoc(checker.getComponentMeta(file), options),
32
+ update(file) {
33
+ if (!SOURCE.test(file)) {
34
+ return
35
+ }
36
+ if (existsSync(file)) {
37
+ checker.updateFile(file, readFileSync(file, 'utf8'))
38
+ }
39
+ else {
40
+ checker.deleteFile(file)
41
+ }
42
+ },
43
+ sources: () => checker.getProgram()?.getSourceFiles().map(sourceFile => sourceFile.fileName).filter(file => !file.includes('/node_modules/')) ?? [],
44
+ // The program is released with the checker; this drops what it cached.
45
+ dispose: () => checker.clearCache(),
46
+ }
47
+ }
@@ -0,0 +1,68 @@
1
+ import type { ComponentDoc, DocEvent, DocProp, DocSlot, DocTag } from '@poveste/shared'
2
+ import type { ComponentMeta, EventMeta, PropertyMeta, SlotMeta } from 'vue-component-meta'
3
+
4
+ export interface VueDocgenOptions {
5
+ /** Packages whose props count although they are declared under `node_modules`. */
6
+ allow?: string[]
7
+ }
8
+
9
+ type Tags = { name: string, text?: string }[]
10
+
11
+ const HIDDEN_TAGS = ['internal', 'private']
12
+ // Aliases: bits-ui mixes them about 50/50, reka-ui and nuxt/ui write `@defaultValue`.
13
+ const DEFAULT_TAGS = ['default', 'defaultValue']
14
+
15
+ function tagsOf(tags: Tags): DocTag[] {
16
+ return tags.map(({ name, text }) => text === undefined ? { name } : { name, text })
17
+ }
18
+
19
+ function isHidden(tags: Tags) {
20
+ return tags.some(tag => HIDDEN_TAGS.includes(tag.name))
21
+ }
22
+
23
+ /** Declared only in installed packages none of which is allowed: an inherited attribute. */
24
+ function isInherited(prop: PropertyMeta, allow: string[]) {
25
+ const files = prop.getDeclarations().map(declaration => declaration.file.replaceAll('\\', '/'))
26
+ return files.length > 0 && files.every(file =>
27
+ file.includes('/node_modules/') && !allow.some(pkg => file.includes(`/node_modules/${pkg}/`)))
28
+ }
29
+
30
+ /** A default as written, without the quoting a tag or the printer added. */
31
+ export function normalizeDefault(value: string) {
32
+ return value.trim().replace(/^`(.*)`$/s, '$1').replace(/^'(.*)'$/s, '"$1"')
33
+ }
34
+
35
+ function toProp(prop: PropertyMeta): DocProp {
36
+ const defaultTag = prop.tags.find(tag => DEFAULT_TAGS.includes(tag.name))?.text
37
+ return {
38
+ name: prop.name,
39
+ ...prop.description ? { description: prop.description } : {},
40
+ type: prop.type,
41
+ required: prop.required,
42
+ ...prop.default !== undefined ? { default: prop.default } : {},
43
+ ...defaultTag !== undefined ? { defaultTag } : {},
44
+ // The code's default is what runs, so it is the one shown; the flag is for the reader.
45
+ ...prop.default !== undefined && defaultTag !== undefined && normalizeDefault(prop.default) !== normalizeDefault(defaultTag)
46
+ ? { defaultConflict: true }
47
+ : {},
48
+ tags: tagsOf(prop.tags),
49
+ }
50
+ }
51
+
52
+ function toMember(member: SlotMeta | EventMeta): DocSlot & DocEvent {
53
+ return {
54
+ name: member.name,
55
+ ...member.description ? { description: member.description } : {},
56
+ ...member.type ? { type: member.type } : {},
57
+ tags: tagsOf(member.tags),
58
+ }
59
+ }
60
+
61
+ export function toComponentDoc(meta: ComponentMeta, options: VueDocgenOptions = {}): ComponentDoc {
62
+ const allow = options.allow ?? []
63
+ return {
64
+ props: meta.props.filter(prop => !prop.global && !isHidden(prop.tags) && !isInherited(prop, allow)).map(toProp),
65
+ slots: meta.slots.filter(slot => !isHidden(slot.tags)).map(toMember),
66
+ events: meta.events.filter(event => !isHidden(event.tags)).map(toMember),
67
+ }
68
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { join } from 'pathe'
4
+
5
+ // `vue-component-meta` 3.3 calls `ts.readJsonConfigFile`, which TypeScript 7 no
6
+ // longer has, and no release of it reads TypeScript 7 yet (#1382).
7
+ const NEWEST_MAJOR = 6
8
+
9
+ const FIX = 'Install `typescript@6` in the book to turn it on, or set `autoDocs: false` to turn it off without this message.'
10
+
11
+ /** The version of the first `typescript` found in `dirs`, searched in order as Node searches `node_modules`. */
12
+ export function typescriptVersionIn(dirs: string[]): string | undefined {
13
+ for (const dir of dirs) {
14
+ const manifest = join(dir, 'typescript', 'package.json')
15
+ if (existsSync(manifest)) {
16
+ return JSON.parse(readFileSync(manifest, 'utf8')).version
17
+ }
18
+ }
19
+ return undefined
20
+ }
21
+
22
+ /** Why auto-docs cannot read a Vue component's types with the TypeScript in `dirs`, or nothing when it can. */
23
+ export function typescriptProblemIn(dirs: string[]): string | undefined {
24
+ const version = typescriptVersionIn(dirs)
25
+ if (!version) {
26
+ return `Auto-docs is off: it reads component types with TypeScript, which this book does not have. ${FIX}`
27
+ }
28
+ if (Number.parseInt(version, 10) > NEWEST_MAJOR) {
29
+ return `Auto-docs is off: it reads component types with TypeScript ${NEWEST_MAJOR} or earlier, and this book has TypeScript ${version}. ${FIX}`
30
+ }
31
+ return undefined
32
+ }
33
+
34
+ /**
35
+ * The same question for the TypeScript `vue-component-meta` will load: it requires
36
+ * `typescript` from where it is installed, an optional peer that npm does not add.
37
+ */
38
+ export function typescriptProblem(): string | undefined {
39
+ const meta = createRequire(import.meta.url).resolve('vue-component-meta')
40
+ return typescriptProblemIn(createRequire(meta).resolve.paths('typescript') ?? [])
41
+ }
package/src/index.node.ts CHANGED
@@ -3,11 +3,14 @@ import type { Plugin } from 'poveste'
3
3
  import { readFileSync } from 'node:fs'
4
4
  import { createRequire } from 'node:module'
5
5
  import { join } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
6
7
 
7
8
  import generateStoryCommand from './commands/generate-story.server.js'
9
+ import { typescriptProblem } from './docgen/typescript.js'
8
10
  import { VUE_SETUP_HOOK_NAMES } from './setup-hooks.js'
9
11
  import { listComponentFiles } from './util/list-components.js'
10
12
  import { VAPOR_INTEROP_ID, vaporInteropModule, withoutVaporPlugin } from './util/vapor.js'
13
+ import { requireVuePlugin } from './util/vue-plugin.js'
11
14
 
12
15
  export function HstVue(): Plugin {
13
16
  return {
@@ -29,6 +32,7 @@ export function HstVue(): Plugin {
29
32
  return {
30
33
  vite: {
31
34
  plugins: [
35
+ requireVuePlugin(),
32
36
  withoutVaporPlugin(),
33
37
  (() => {
34
38
  let root = process.cwd()
@@ -59,6 +63,13 @@ export function HstVue(): Plugin {
59
63
  }
60
64
  },
61
65
 
66
+ // A story file documents nothing: it names the components that do.
67
+ docgen: {
68
+ match: file => file.endsWith('.vue') && !file.endsWith('.story.vue'),
69
+ module: fileURLToPath(new URL('./docgen/extractor.js', import.meta.url)),
70
+ unavailable: () => typescriptProblem(),
71
+ },
72
+
62
73
  supportPlugin: {
63
74
  id: 'vue3',
64
75
  moduleName: '@poveste/plugin-vue',
@@ -0,0 +1,32 @@
1
+ import type { Plugin as VitePlugin } from 'vite'
2
+ import { existsSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ const QUASAR_CONFIGS = ['quasar.config.js', 'quasar.config.mjs', 'quasar.config.cjs', 'quasar.config.ts']
6
+
7
+ /**
8
+ * What to say when a book's Vite config has no Vue plugin. Without one, Vite
9
+ * answers the first `.vue` file with "Install @vitejs/plugin-vue", which sends a
10
+ * Quasar reader to install a package their project does not need: Quasar brings
11
+ * its own Vue plugin, and `HstQuasar()` is what hands it to Poveste (#1337).
12
+ */
13
+ export function noVuePluginMessage(root: string, exists: (path: string) => boolean = existsSync): string {
14
+ const fix = QUASAR_CONFIGS.some(name => exists(join(root, name)))
15
+ ? 'This is a Quasar project: add `HstQuasar()` from `@poveste/plugin-quasar` to the config\'s `plugins`, which brings Quasar\'s own Vue plugin.'
16
+ : 'Add `@vitejs/plugin-vue` to `vite.config`, or in a Nuxt project `HstNuxt()` from `@poveste/plugin-nuxt` to the config\'s `plugins`.'
17
+ return `[poveste] The Vite config has no Vue plugin, so no \`.vue\` story can be compiled. ${fix}`
18
+ }
19
+
20
+ /** Reports, once per process, a resolved Vite config with no Vue plugin in it. */
21
+ export function requireVuePlugin(): VitePlugin {
22
+ let reported = false
23
+ return {
24
+ name: 'poveste-plugin-vue:require-vue-plugin',
25
+ configResolved(config) {
26
+ if (!reported && !config.plugins.some(plugin => plugin.name === 'vite:vue')) {
27
+ reported = true
28
+ console.error(noVuePluginMessage(config.root))
29
+ }
30
+ },
31
+ }
32
+ }