@poveste/plugin-vue 0.2.1 → 0.3.1

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,79 +1,75 @@
1
- import { defineComponent as a, inject as r, provide as i, onMounted as d } from "vue";
2
- import { autoStubComponents as l } from "./stub.js";
3
- const f = a({
4
- name: "PovesteStory",
5
- inheritAttrs: !1,
6
- props: {
7
- title: {
8
- type: String,
9
- default: void 0
10
- },
11
- id: {
12
- type: String,
13
- default: void 0
14
- },
15
- group: {
16
- type: String,
17
- default: void 0
18
- },
19
- layout: {
20
- type: Object,
21
- default: void 0
22
- },
23
- icon: {
24
- type: String,
25
- default: void 0
26
- },
27
- iconColor: {
28
- type: String,
29
- default: void 0
30
- },
31
- docsOnly: {
32
- type: Boolean,
33
- default: !1
34
- },
35
- meta: {
36
- type: Object,
37
- default: void 0
38
- }
39
- },
40
- setup(t) {
41
- const e = r("hstStoryFile"), o = {
42
- id: t.id ?? e?.id,
43
- title: t.title ?? e?.fileName,
44
- group: t.group,
45
- layout: t.layout,
46
- icon: t.icon,
47
- iconColor: t.iconColor,
48
- docsOnly: t.docsOnly,
49
- meta: t.meta,
50
- variants: []
51
- };
52
- return r("addStory", null)?.(o), i("story", o), i("addVariant", (n) => {
53
- o.variants.push(n);
54
- }), d(() => {
55
- o.variants.length || o.variants.push({
56
- id: "_default",
57
- title: "default"
58
- });
59
- }), {
60
- story: o
61
- };
62
- },
63
- render() {
64
- let t = !1;
65
- try {
66
- const e = this.$slots.default?.({
67
- get state() {
68
- return t = !0, {};
69
- }
70
- });
71
- return Array.isArray(e) && l(e), e;
72
- } catch (e) {
73
- return t || console.error(e), null;
74
- }
75
- }
1
+ import { autoStubComponents as e } from "./stub.js";
2
+ import { defineComponent as t, inject as n, onMounted as r, provide as i } from "vue";
3
+ //#region src/client/server/Story.ts
4
+ var a = t({
5
+ name: "PovesteStory",
6
+ inheritAttrs: !1,
7
+ props: {
8
+ title: {
9
+ type: String,
10
+ default: void 0
11
+ },
12
+ id: {
13
+ type: String,
14
+ default: void 0
15
+ },
16
+ group: {
17
+ type: String,
18
+ default: void 0
19
+ },
20
+ layout: {
21
+ type: Object,
22
+ default: void 0
23
+ },
24
+ icon: {
25
+ type: String,
26
+ default: void 0
27
+ },
28
+ iconColor: {
29
+ type: String,
30
+ default: void 0
31
+ },
32
+ docsOnly: {
33
+ type: Boolean,
34
+ default: !1
35
+ },
36
+ meta: {
37
+ type: Object,
38
+ default: void 0
39
+ }
40
+ },
41
+ setup(e) {
42
+ let t = n("hstStoryFile"), a = {
43
+ id: e.id ?? t?.id,
44
+ title: e.title ?? t?.fileName,
45
+ group: e.group,
46
+ layout: e.layout,
47
+ icon: e.icon,
48
+ iconColor: e.iconColor,
49
+ docsOnly: e.docsOnly,
50
+ meta: e.meta,
51
+ variants: []
52
+ };
53
+ return n("addStory", null)?.(a), i("story", a), i("addVariant", (e) => {
54
+ a.variants.push(e);
55
+ }), r(() => {
56
+ a.variants.length || a.variants.push({
57
+ id: "_default",
58
+ title: "default"
59
+ });
60
+ }), { story: a };
61
+ },
62
+ render() {
63
+ let t = !1;
64
+ try {
65
+ let n = this.$slots.default?.({ get state() {
66
+ return t = !0, {};
67
+ } });
68
+ return Array.isArray(n) && e(n), n;
69
+ } catch (e) {
70
+ return t || console.error(e), null;
71
+ }
72
+ }
76
73
  });
77
- export {
78
- f as default
79
- };
74
+ //#endregion
75
+ export { a as default };
@@ -1,46 +1,46 @@
1
- import { defineComponent as o, inject as n } from "vue";
2
- const l = o({
3
- name: "PovesteVariant",
4
- props: {
5
- title: {
6
- type: String,
7
- default: "untitled"
8
- },
9
- id: {
10
- type: String,
11
- default: void 0
12
- },
13
- icon: {
14
- type: String,
15
- default: void 0
16
- },
17
- iconColor: {
18
- type: String,
19
- default: void 0
20
- },
21
- meta: {
22
- type: Object,
23
- default: void 0
24
- }
25
- },
26
- setup(t) {
27
- const e = n("story");
28
- function i() {
29
- return `${e.id}-${e.variants.length}`;
30
- }
31
- const a = {
32
- id: t.id ?? i(),
33
- title: t.title,
34
- icon: t.icon,
35
- iconColor: t.iconColor,
36
- meta: t.meta
37
- };
38
- n("addVariant")(a);
39
- },
40
- render() {
41
- return null;
42
- }
1
+ import { defineComponent as e, inject as t } from "vue";
2
+ //#region src/client/server/Variant.ts
3
+ var n = e({
4
+ name: "PovesteVariant",
5
+ props: {
6
+ title: {
7
+ type: String,
8
+ default: "untitled"
9
+ },
10
+ id: {
11
+ type: String,
12
+ default: void 0
13
+ },
14
+ icon: {
15
+ type: String,
16
+ default: void 0
17
+ },
18
+ iconColor: {
19
+ type: String,
20
+ default: void 0
21
+ },
22
+ meta: {
23
+ type: Object,
24
+ default: void 0
25
+ }
26
+ },
27
+ setup(e) {
28
+ let n = t("story");
29
+ function r() {
30
+ return `${n.id}-${n.variants.length}`;
31
+ }
32
+ let i = {
33
+ id: e.id ?? r(),
34
+ title: e.title,
35
+ icon: e.icon,
36
+ iconColor: e.iconColor,
37
+ meta: e.meta
38
+ };
39
+ t("addVariant")(i);
40
+ },
41
+ render() {
42
+ return null;
43
+ }
43
44
  });
44
- export {
45
- l as default
46
- };
45
+ //#endregion
46
+ export { n as default };
@@ -1,44 +1,43 @@
1
+ import e from "./Story.js";
2
+ import t from "./Variant.js";
3
+ import { createApp as n, h as r } from "vue";
1
4
  import { getSetupHook as i } from "@poveste/shared";
2
- import * as d from "virtual:$poveste-generated-global-setup";
3
- import * as f from "virtual:$poveste-setup";
4
- import { createApp as l, h as S } from "vue";
5
- import y from "./Story.js";
6
- import h from "./Variant.js";
7
- async function w({ file: n, storyData: r, el: m }) {
8
- const { default: e } = await import(/* @vite-ignore */
9
- /* @vite-ignore */
10
- n.moduleId
11
- ), t = l({
12
- provide: {
13
- addStory(o) {
14
- r.push(o);
15
- },
16
- hstStoryFile: n
17
- },
18
- render() {
19
- return S(e);
20
- }
21
- });
22
- t.component("Story", y), t.component("Variant", h);
23
- const p = {
24
- app: t,
25
- story: null,
26
- variant: null,
27
- addWrapper: () => {
28
- }
29
- }, a = i(d, "setupVue3");
30
- a && await a(p);
31
- const u = i(f, "setupVue3");
32
- if (u && await u(p), t.mount(m), e.doc) {
33
- const o = document.createElement("div");
34
- o.innerHTML = e.doc;
35
- const s = o.textContent;
36
- r.forEach((c) => {
37
- c.docsText = s;
38
- });
39
- }
40
- t.unmount();
5
+ import * as a from "virtual:$poveste-generated-global-setup";
6
+ import * as o from "virtual:$poveste-setup";
7
+ //#region src/client/server/run.ts
8
+ async function s({ file: s, storyData: c, el: l }) {
9
+ let { default: u } = await import(/* @vite-ignore */
10
+ /* @vite-ignore */
11
+ s.moduleId
12
+ ), d = n({
13
+ provide: {
14
+ addStory(e) {
15
+ c.push(e);
16
+ },
17
+ hstStoryFile: s
18
+ },
19
+ render() {
20
+ return r(u);
21
+ }
22
+ });
23
+ d.component("Story", e), d.component("Variant", t);
24
+ let f = {
25
+ app: d,
26
+ story: null,
27
+ variant: null,
28
+ addWrapper: () => {}
29
+ }, p = i(a, "setupVue3");
30
+ p && await p(f);
31
+ let m = i(o, "setupVue3");
32
+ if (m && await m(f), d.mount(l), u.doc) {
33
+ let e = document.createElement("div");
34
+ e.innerHTML = u.doc;
35
+ let t = e.textContent;
36
+ c.forEach((e) => {
37
+ e.docsText = t;
38
+ });
39
+ }
40
+ d.unmount();
41
41
  }
42
- export {
43
- w as run
44
- };
42
+ //#endregion
43
+ export { s as run };
@@ -1,8 +1,10 @@
1
- const n = { name: "StubbedComponent", render: () => null };
2
- function o(t) {
3
- for (const e of t)
4
- typeof e.type == "object" && e.type.name !== "PovesteVariant" && (e.type = n), Array.isArray(e.children) && o(e.children);
5
- }
6
- export {
7
- o as autoStubComponents
1
+ //#region src/client/server/stub.ts
2
+ var e = {
3
+ name: "StubbedComponent",
4
+ render: () => null
8
5
  };
6
+ function t(n) {
7
+ for (let r of n) typeof r.type == "object" && r.type.name !== "PovesteVariant" && (r.type = e), Array.isArray(r.children) && t(r.children);
8
+ }
9
+ //#endregion
10
+ export { t as autoStubComponents };
@@ -1,4 +1,2 @@
1
1
  import { run as e } from "./server/run.js";
2
- export {
3
- e as run
4
- };
2
+ export { e as run };
@@ -27,7 +27,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
27
27
  default: any;
28
28
  };
29
29
  }>> & Readonly<{}>, {
30
- meta: import("@poveste/shared").StoryMeta;
30
+ meta: import("poveste").StoryMeta;
31
31
  initState: () => any | Promise<any>;
32
32
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
33
33
  export default _default;
@@ -1,5 +1,5 @@
1
1
  import type { Story, Variant } from '@poveste/shared';
2
- import { type PreviewRenderContext } from './render-context.js';
2
+ import type { PreviewRenderContext } from './render-context.js';
3
3
  interface PreviewHostOptions {
4
4
  name: string;
5
5
  el: HTMLElement;
@@ -69,11 +69,11 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
69
69
  default: any;
70
70
  };
71
71
  }>> & Readonly<{}>, {
72
- meta: import("@poveste/shared").StoryMeta;
72
+ meta: import("poveste").StoryMeta;
73
73
  title: string;
74
74
  id: string;
75
75
  group: string;
76
- layout: import("@poveste/shared").StoryLayout;
76
+ layout: import("poveste").StoryLayout;
77
77
  icon: string;
78
78
  iconColor: string;
79
79
  docsOnly: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { ServerVariant } from '@poveste/shared';
2
- import { type PropType } from 'vue';
2
+ import type { PropType } from 'vue';
3
3
  declare const _default: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
4
  title: {
5
5
  type: StringConstructor;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/plugin-vue",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.3.1",
5
5
  "description": "Poveste plugin for Vue.js support",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -44,28 +44,31 @@
44
44
  "types": "dist/index.node.d.ts",
45
45
  "peerDependencies": {
46
46
  "vue": "^3.5.26",
47
- "poveste": "^0.2.1"
47
+ "poveste": "^0.3.1"
48
48
  },
49
49
  "dependencies": {
50
50
  "change-case": "^5.4.4",
51
51
  "globby": "^14.0.2",
52
52
  "launch-editor": "^2.9.1",
53
53
  "pathe": "^1.1.2",
54
- "@poveste/controls": "^0.2.1",
55
- "@poveste/vendors": "^0.2.1",
56
- "@poveste/shared": "^0.2.1"
54
+ "@poveste/controls": "^0.3.1",
55
+ "@poveste/shared": "^0.3.1",
56
+ "@poveste/vendors": "^0.3.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.1",
60
60
  "concurrently": "^9.1.0",
61
61
  "typescript": "5.6.3",
62
- "vite": "^7.3.0",
62
+ "vite": "^8.2.0",
63
+ "vitest": "^4.1.10",
63
64
  "vue": "^3.5.26",
64
- "poveste": "0.2.1"
65
+ "poveste": "0.3.1"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "rimraf dist && vite build && tsc -d -P tsconfig.build.json && pnpm run build:types",
68
69
  "build:types": "tsc --declaration --emitDeclarationOnly",
69
- "watch": "concurrently \"vite build --watch\" \"tsc -d -P tsconfig.build.json --watch\" \"pnpm run build:types --watch\""
70
+ "watch": "concurrently \"vite build --watch\" \"tsc -d -P tsconfig.build.json --watch\" \"pnpm run build:types --watch\"",
71
+ "test": "vitest run",
72
+ "test:dev": "vitest"
70
73
  }
71
74
  }
@@ -61,6 +61,20 @@ export default defineComponent({
61
61
  let lastPropsTypesSnapshot: string
62
62
  let renderVariant: Variant
63
63
  let renderStateSync: ReturnType<typeof syncStateBundledAndExternal>
64
+ let mountStateSync: ReturnType<typeof syncStateBundledAndExternal>
65
+
66
+ // Registered before the first `await` below, and deliberately so. This is
67
+ // an async setup(), and Vue drops any lifecycle hook registered after an
68
+ // await has actually suspended — it warns, then the hook is bound to no
69
+ // instance and never runs. That is how both state syncs below used to
70
+ // survive unmount: a story with `initState` suspends here, so the teardown
71
+ // that used to sit at the bottom of setup() was silently discarded, and
72
+ // every leaked pair kept deep-watching the variant state it was created
73
+ // for. See #77.
74
+ onBeforeUnmount(() => {
75
+ renderStateSync?.stop()
76
+ mountStateSync?.stop()
77
+ })
64
78
 
65
79
  const mountVariant = computed(() => attrs.variant)
66
80
 
@@ -70,7 +84,7 @@ export default defineComponent({
70
84
  }
71
85
 
72
86
  if (renderContext?.mode !== 'render' && mountVariant.value && implicitState) {
73
- syncStateBundledAndExternal(mountVariant.value.state, implicitState())
87
+ mountStateSync = syncStateBundledAndExternal(mountVariant.value.state, implicitState())
74
88
  }
75
89
 
76
90
  function updateVariant(variant: Variant) {
@@ -160,10 +174,6 @@ export default defineComponent({
160
174
  updateVariant(mountVariant.value)
161
175
  }
162
176
 
163
- onBeforeUnmount(() => {
164
- renderStateSync?.stop()
165
- })
166
-
167
177
  return {
168
178
  renderContext,
169
179
  renderVariantSlot,
@@ -0,0 +1,154 @@
1
+ import { nextTick as bundledNextTick, reactive as bundledReactive } from '@poveste/vendors/vue'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { nextTick, reactive } from 'vue'
4
+ import { syncStateBundledAndExternal } from '../util.js'
5
+
6
+ // These run against both Vue copies for real: `@poveste/vendors/vue` is the one
7
+ // poveste bundles, `vue` is the one a user's story would run on. That is the
8
+ // whole point — the sync exists because the two have separate reactivity graphs
9
+ // and separate schedulers, and nothing about it can be tested with one of them.
10
+
11
+ // Each side flushes on its own scheduler and a write can bounce between them a
12
+ // few times before settling, so drain both alternately rather than once.
13
+ async function settle() {
14
+ for (let i = 0; i < 6; i++) {
15
+ await nextTick()
16
+ await bundledNextTick()
17
+ }
18
+ }
19
+
20
+ function setup(initial: Record<string, any>) {
21
+ const bundled = bundledReactive(structuredClone(initial))
22
+ const external = reactive(structuredClone(initial))
23
+ const sync = syncStateBundledAndExternal(bundled, external)
24
+ return { bundled, external, sync }
25
+ }
26
+
27
+ describe('syncStateBundledAndExternal', () => {
28
+ it('mirrors an edit in each direction', async () => {
29
+ const { bundled, external, sync } = setup({ count: 0 })
30
+ await settle()
31
+
32
+ external.count = 1
33
+ await settle()
34
+ expect(bundled.count).toBe(1)
35
+
36
+ bundled.count = 2
37
+ await settle()
38
+ expect(external.count).toBe(2)
39
+
40
+ sync.stop()
41
+ })
42
+
43
+ it('stops mirroring once stopped', async () => {
44
+ const { bundled, external, sync } = setup({ count: 0 })
45
+ await settle()
46
+ sync.stop()
47
+
48
+ external.count = 1
49
+ await settle()
50
+ expect(bundled.count).toBe(0)
51
+ })
52
+
53
+ // The #95 regression. `applyState` iterates the incoming keys, so it has no
54
+ // way to express "this key is gone" — the write it performs for a removal
55
+ // changes nothing on the far side, and the far side's watcher never fires.
56
+ //
57
+ // That matters because the two watchers coordinate through a `syncing` boolean
58
+ // meaning "ignore the next firing, it is my own echo", and the counterpart
59
+ // firing is what clears it. No counterpart, no clear: the flag stayed set, and
60
+ // the next edit was mistaken for an echo and dropped on the floor.
61
+ //
62
+ // Worth being exact about how bad this was in the shipped app, because it is
63
+ // the reason the issue had no repro: it never bit here. Vue stories always
64
+ // carry `_hPropState` and `_hPropDefs`, and `applyState` sent every
65
+ // `_h`-prefixed key down the plain-assignment branch, so those two were
66
+ // re-identified on every apply and a counterpart firing was guaranteed. The
67
+ // invariant held by luck. These tests use a state without that bookkeeping,
68
+ // which is what the function actually promises to handle — and which is the
69
+ // shape the sandbox bridge and `plugin-svelte` were relying on luck for too.
70
+ it('does not drop the edit after a removal it cannot mirror', async () => {
71
+ const { bundled, external, sync } = setup({ count: 0, gone: 1 })
72
+ await settle()
73
+
74
+ delete external.gone
75
+ await settle()
76
+
77
+ external.count = 1
78
+ await settle()
79
+ expect(bundled.count).toBe(1)
80
+
81
+ sync.stop()
82
+ })
83
+
84
+ it('does not drop the edit after a nested removal it cannot mirror', async () => {
85
+ // The same shape one level down, and the more plausible one: a story that
86
+ // drops an entry out of a map in state, then touches a control.
87
+ const { bundled, external, sync } = setup({ count: 0, items: { a: 1, b: 2 } })
88
+ await settle()
89
+
90
+ delete external.items.b
91
+ await settle()
92
+
93
+ external.count = 1
94
+ await settle()
95
+ expect(bundled.count).toBe(1)
96
+
97
+ sync.stop()
98
+ })
99
+
100
+ it('does not drop the edit after a change that nets out to nothing', async () => {
101
+ const { bundled, external, sync } = setup({ count: 0, list: [1, 2] })
102
+ await settle()
103
+
104
+ external.list.push(9)
105
+ external.list.pop()
106
+ await settle()
107
+
108
+ external.count = 1
109
+ await settle()
110
+ expect(bundled.count).toBe(1)
111
+
112
+ sync.stop()
113
+ })
114
+
115
+ it('leaves object identity alone on both sides when a sync changes nothing', async () => {
116
+ // The other half of the fix, and the reason the flag can now be trusted.
117
+ // `toRawDeep` rebuilds every object it passes, so mirroring `count` used to
118
+ // drop a fresh `nested` and `list` onto the receiving side as well, and each
119
+ // of those re-triggered its watchers. Every sync was a burst of firings that
120
+ // happened to keep the count even; none of them meant anything.
121
+ const { bundled, external, sync } = setup({ count: 0, nested: { a: 1 }, list: [1, 2] })
122
+ await settle()
123
+
124
+ const externalNested = external.nested
125
+ const externalList = external.list
126
+ const bundledNested = bundled.nested
127
+ const bundledList = bundled.list
128
+
129
+ external.count = 1
130
+ await settle()
131
+
132
+ expect(bundled.count).toBe(1)
133
+ expect(external.nested).toBe(externalNested)
134
+ expect(external.list).toBe(externalList)
135
+ // The receiving side is the one that used to get churned.
136
+ expect(bundled.nested).toBe(bundledNested)
137
+ expect(bundled.list).toBe(bundledList)
138
+
139
+ sync.stop()
140
+ })
141
+
142
+ it('mirrors many edits in a row without falling behind', async () => {
143
+ const { bundled, external, sync } = setup({ count: 0 })
144
+ await settle()
145
+
146
+ for (let i = 1; i <= 10; i++) {
147
+ external.count = i
148
+ await settle()
149
+ expect(bundled.count).toBe(i)
150
+ }
151
+
152
+ sync.stop()
153
+ })
154
+ })
@@ -1,6 +1,7 @@
1
1
  import type { Story, Variant } from '@poveste/shared'
2
2
  import type { App, Component, PropType, VNode } from 'vue'
3
3
  import type { Vue3StorySetupApi, Vue3StorySetupHandler } from '../../helpers.js'
4
+ import type { PreviewRenderContext } from './render-context.js'
4
5
  import { getSetupHook } from '@poveste/shared'
5
6
  // @ts-expect-error virtual module id
6
7
  import * as generatedSetup from 'virtual:$poveste-generated-global-setup'
@@ -13,7 +14,7 @@ import {
13
14
  Suspense,
14
15
  } from 'vue'
15
16
  import { registerGlobalComponents } from './global-components.js'
16
- import { type PreviewRenderContext, provideRenderContext } from './render-context.js'
17
+ import { provideRenderContext } from './render-context.js'
17
18
  import { RouterLinkStub } from './RouterLinkStub'
18
19
 
19
20
  interface PreviewHostOptions {