@poveste/plugin-vue 0.14.0 → 0.16.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 (68) hide show
  1. package/README.md +2 -2
  2. package/components.d.ts +4 -0
  3. package/dist/bundled/client/app/MountStory.js +8 -3
  4. package/dist/bundled/client/app/RenderStory.js +15 -9
  5. package/dist/bundled/client/app/Story.js +76 -62
  6. package/dist/bundled/client/app/Variant.js +48 -42
  7. package/dist/bundled/client/app/context.js +5 -0
  8. package/dist/bundled/client/app/global-components.js +11 -10
  9. package/dist/bundled/client/app/host.js +2 -2
  10. package/dist/bundled/client/app/render-context.js +7 -7
  11. package/dist/bundled/client/app/util.js +36 -26
  12. package/dist/bundled/client/client.js +4 -4
  13. package/dist/bundled/client/codegen.js +79 -60
  14. package/dist/bundled/client/context.js +20 -0
  15. package/dist/bundled/client/server/Story.js +28 -25
  16. package/dist/bundled/client/server/Variant.js +13 -16
  17. package/dist/bundled/client/server/context.js +5 -0
  18. package/dist/bundled/client/server/run.js +29 -29
  19. package/dist/client/app/MountStory.d.ts +12 -3
  20. package/dist/client/app/RenderStory.d.ts +1 -1
  21. package/dist/client/app/Story.d.ts +10 -7
  22. package/dist/client/app/Variant.d.ts +14 -13
  23. package/dist/client/app/context.d.ts +9 -0
  24. package/dist/client/app/render-context.d.ts +16 -1
  25. package/dist/client/app/util.d.ts +38 -3
  26. package/dist/client/codegen.d.ts +29 -0
  27. package/dist/client/context.d.ts +40 -0
  28. package/dist/client/server/Story.d.ts +17 -17
  29. package/dist/client/server/Variant.d.ts +10 -10
  30. package/dist/client/server/context.d.ts +7 -0
  31. package/dist/commands/generate-story.client.js +4 -4
  32. package/dist/commands/generate-story.server.js +2 -2
  33. package/dist/helpers.d.ts +2 -2
  34. package/dist/index.node.js +2 -0
  35. package/dist/util/list-components.d.ts +1 -1
  36. package/dist/util/list-components.js +63 -6
  37. package/dist/util/list-components.spec.d.ts +1 -0
  38. package/dist/util/list-components.spec.js +46 -0
  39. package/package.json +17 -16
  40. package/src/client/__tests__/codegen-attrs.spec.ts +149 -0
  41. package/src/client/__tests__/context.spec.ts +50 -0
  42. package/src/client/app/MountStory.ts +11 -1
  43. package/src/client/app/RenderStory.ts +6 -1
  44. package/src/client/app/RouterLinkStub.ts +1 -1
  45. package/src/client/app/Story.ts +85 -39
  46. package/src/client/app/Variant.ts +35 -27
  47. package/src/client/app/__tests__/preview-variants.spec.ts +76 -0
  48. package/src/client/app/__tests__/state-sync.spec.ts +113 -2
  49. package/src/client/app/__tests__/writable-binding.spec.ts +39 -0
  50. package/src/client/app/auto-props.ts +2 -2
  51. package/src/client/app/context.ts +17 -0
  52. package/src/client/app/global-components.ts +20 -14
  53. package/src/client/app/host.ts +9 -6
  54. package/src/client/app/render-context.ts +25 -5
  55. package/src/client/app/util.ts +104 -9
  56. package/src/client/codegen.ts +81 -29
  57. package/src/client/context.ts +66 -0
  58. package/src/client/server/Story.ts +16 -10
  59. package/src/client/server/Variant.ts +13 -14
  60. package/src/client/server/__tests__/nesting-errors.spec.ts +48 -0
  61. package/src/client/server/context.ts +18 -0
  62. package/src/client/server/run.ts +11 -7
  63. package/src/commands/generate-story.client.ts +4 -4
  64. package/src/commands/generate-story.server.ts +2 -2
  65. package/src/helpers.ts +2 -2
  66. package/src/index.node.ts +2 -0
  67. package/src/util/list-components.spec.ts +54 -0
  68. package/src/util/list-components.ts +68 -6
@@ -0,0 +1,149 @@
1
+ import type { Variant } from '@poveste/shared'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { h } from 'vue'
4
+ import { delimitAttr, generateSourceCode } from '../codegen.js'
5
+
6
+ /*
7
+ * The source pane shows the template that produced the variant, so a handler
8
+ * has to appear as its author wrote it. The pass this replaces escaped `'` for
9
+ * a JavaScript string and rewrote `"` into `'` — neither of which is the
10
+ * attribute the result lands in — and each half reached the reader alone (#602).
11
+ *
12
+ * A case carrying both quote characters is the one that matters: each half
13
+ * fails on its own, so a fixture with only one of them passes a half-fix.
14
+ */
15
+ describe('delimitAttr', () => {
16
+ it('leaves code with no quotes in it alone, under the usual delimiter', () => {
17
+ expect(delimitAttr(['onClick'])).toEqual({ quote: '"', lines: ['onClick'] })
18
+ })
19
+
20
+ // Was shown as `() => alert('hi')` — the reader's double quotes rewritten.
21
+ it('takes the other delimiter rather than rewriting a double quote', () => {
22
+ expect(delimitAttr(['() => alert("hi")'])).toEqual({
23
+ quote: '\'',
24
+ lines: ['() => alert("hi")'],
25
+ })
26
+ })
27
+
28
+ // Was shown as `() => alert(\'hi\')`, backslashes and all.
29
+ it('leaves a single quote unescaped, since the attribute is not a JS string', () => {
30
+ expect(delimitAttr(['() => alert(\'hi\')'])).toEqual({
31
+ quote: '"',
32
+ lines: ['() => alert(\'hi\')'],
33
+ })
34
+ })
35
+
36
+ // Both characters, so whichever delimiter is chosen has to be escaped inside
37
+ // it. Two doubles against one single here, so the single is the cheaper one.
38
+ it('escapes the quote that appears less often when the code carries both', () => {
39
+ expect(delimitAttr(['() => console.log("it\'s here")'])).toEqual({
40
+ quote: '\'',
41
+ lines: ['() => console.log("it's here")'],
42
+ })
43
+ })
44
+
45
+ // Four singles against two doubles, so the double is the cheaper one here.
46
+ it('escapes the double quote instead when that is the rarer one', () => {
47
+ expect(delimitAttr(['() => alert(\'a\', \'b\', "c")'])).toEqual({
48
+ quote: '"',
49
+ lines: ['() => alert(\'a\', \'b\', "c")'],
50
+ })
51
+ })
52
+
53
+ // The alert CodeQL raised: the escaping pass never escaped a backslash, so
54
+ // `() => alert('it\'s')` went in carrying one and came out carrying four.
55
+ it('passes a backslash through untouched', () => {
56
+ const written = '() => alert(\'it\\\'s\')'
57
+
58
+ expect(delimitAttr([written]).lines[0]).toBe(written)
59
+ })
60
+
61
+ it('reads the whole value when deciding, not one line of it', () => {
62
+ expect(delimitAttr(['() => {', ' alert("hi")', '}']).quote).toBe('\'')
63
+ })
64
+
65
+ it('escapes on every line, not only the one that decided it', () => {
66
+ expect(delimitAttr(['() => {', ' alert("a")', ' alert(\'b\')', ' alert(\'c\')', '}']).lines).toEqual([
67
+ '() => {',
68
+ ' alert("a")',
69
+ ' alert(\'b\')',
70
+ ' alert(\'c\')',
71
+ '}',
72
+ ])
73
+ })
74
+ })
75
+
76
+ /*
77
+ * `delimitAttr` could be right and unused, so these drive the generator itself.
78
+ *
79
+ * The handler states its own source rather than being written as one. A `.vue`
80
+ * ships to the browser untransformed, but this spec does not: esbuild rewrote
81
+ * `alert('it\'s')` in the fixture to `alert("it's")` before `toString()` ever
82
+ * saw it, so a handler written here is not the text the generator receives.
83
+ * Overriding `toString` is what makes the input exact.
84
+ */
85
+ const WITH_DOUBLE = '() => alert("hi")'
86
+ const WITH_SINGLE = '() => alert(\'hi\')'
87
+ const WITH_BOTH = '() => console.log("it\'s here")'
88
+ const WITH_BACKSLASH = String.raw`() => alert('it\'s')`
89
+
90
+ function handlerWritten(source: string): () => void {
91
+ const handler = (): void => {}
92
+ handler.toString = () => source
93
+ return handler
94
+ }
95
+
96
+ function variantWith(source: string): Variant {
97
+ return {
98
+ state: {},
99
+ slots: () => ({ default: () => h('button', { onClick: handlerWritten(source) }, 'go') }),
100
+ } as unknown as Variant
101
+ }
102
+
103
+ describe('generateSourceCode, for an event handler', () => {
104
+ it('shows a double quote the author wrote, under the other delimiter', async () => {
105
+ expect(await generateSourceCode(variantWith(WITH_DOUBLE))).toContain(`@click='${WITH_DOUBLE}'`)
106
+ })
107
+
108
+ it('shows a single quote unescaped, under the usual delimiter', async () => {
109
+ expect(await generateSourceCode(variantWith(WITH_SINGLE))).toContain(`@click="${WITH_SINGLE}"`)
110
+ })
111
+
112
+ it('escapes only the rarer quote when the handler carries both', async () => {
113
+ expect(await generateSourceCode(variantWith(WITH_BOTH)))
114
+ .toContain('@click=\'() => console.log("it's here")\'')
115
+ })
116
+
117
+ it('shows a backslash the author wrote exactly as written', async () => {
118
+ expect(await generateSourceCode(variantWith(WITH_BACKSLASH))).toContain(WITH_BACKSLASH)
119
+ })
120
+
121
+ // The alert itself: one backslash in, four out.
122
+ it('does not multiply the backslash it was given', async () => {
123
+ const source = await generateSourceCode(variantWith(WITH_BACKSLASH))
124
+
125
+ expect(source.match(/\\/g)).toHaveLength(1)
126
+ })
127
+ })
128
+
129
+ /*
130
+ * A string prop is the other way a `"` reaches an attribute, and it took the
131
+ * delimiter fix later than the handlers did — the element below rendered its
132
+ * directives correctly and mangled this one.
133
+ */
134
+ describe('generateSourceCode, for a string prop', () => {
135
+ function variantWithTitle(title: string): Variant {
136
+ return {
137
+ state: {},
138
+ slots: () => ({ default: () => h('div', { title }, 'go') }),
139
+ } as unknown as Variant
140
+ }
141
+
142
+ it('leaves a plain value under the usual delimiter', async () => {
143
+ expect(await generateSourceCode(variantWithTitle('a calm title'))).toContain('title="a calm title"')
144
+ })
145
+
146
+ it('takes the other delimiter rather than closing the attribute early', async () => {
147
+ expect(await generateSourceCode(variantWithTitle('He said "hi"'))).toContain(`title='He said "hi"'`)
148
+ })
149
+ })
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ /*
4
+ * A collection worker evaluates the context modules more than once — its graph is
5
+ * invalidated while a story is mid-run — so the provider and the consumer can each
6
+ * hold their own instance of one file. The key is what has to survive that: minted
7
+ * per instance it differs between them, the injection misses, and the reader is
8
+ * told their `<Variant>` is outside a `<Story>`.
9
+ *
10
+ * Vue's own `provide`/`inject` are stubbed because they need a mounted component
11
+ * and this package's tests have no DOM; the key they are handed is the subject.
12
+ */
13
+ const provided: unknown[] = []
14
+ const injected: unknown[] = []
15
+
16
+ vi.mock('vue', () => ({
17
+ provide: (key: unknown) => {
18
+ provided.push(key)
19
+ },
20
+ inject: (key: unknown) => {
21
+ injected.push(key)
22
+ return null
23
+ },
24
+ }))
25
+
26
+ const { createContext } = await import('../context.js')
27
+
28
+ function keyOf(description: string) {
29
+ createContext(description, '<Story>').provide('anything')
30
+ return provided.at(-1)
31
+ }
32
+
33
+ describe('createContext', () => {
34
+ it('gives two instances of the same context one key', () => {
35
+ expect(keyOf('poveste-test-context')).toBe(keyOf('poveste-test-context'))
36
+ })
37
+
38
+ it('keeps two contexts apart when their descriptions differ', () => {
39
+ // What #981 was: one key meaning two things.
40
+ expect(keyOf('poveste-test-one')).not.toBe(keyOf('poveste-test-two'))
41
+ })
42
+
43
+ it('injects under the key it provides under', () => {
44
+ const context = createContext<string>('poveste-test-round-trip', '<Story>')
45
+ context.provide('the story file')
46
+ context.injectOptional()
47
+
48
+ expect(injected.at(-1)).toBe(provided.at(-1))
49
+ })
50
+ })
@@ -26,6 +26,11 @@ export default _defineComponent({
26
26
  type: String,
27
27
  default: null,
28
28
  },
29
+
30
+ syncState: {
31
+ type: Boolean,
32
+ default: true,
33
+ },
29
34
  },
30
35
 
31
36
  setup(props) {
@@ -35,14 +40,19 @@ export default _defineComponent({
35
40
  slotName: 'default',
36
41
  currentVariant: null,
37
42
  targetVariantId: props.targetVariantId,
43
+ syncState: props.syncState,
38
44
  externalState: null,
39
45
  nextVariantIndex: {
40
46
  value: 0,
41
47
  },
42
48
  })
43
- let host: ReturnType<typeof createPreviewHost>
49
+ let host: ReturnType<typeof createPreviewHost> | null = null
44
50
 
45
51
  async function mountStory() {
52
+ if (!el.value) {
53
+ return
54
+ }
55
+
46
56
  host = createPreviewHost({
47
57
  name: 'MountStorySubApp',
48
58
  el: el.value,
@@ -40,7 +40,7 @@ export default _defineComponent({
40
40
 
41
41
  setup(props, { emit }) {
42
42
  const sandbox = _ref<HTMLDivElement>()
43
- let host: ReturnType<typeof createPreviewHost>
43
+ let host: ReturnType<typeof createPreviewHost> | null = null
44
44
  let mounting = false
45
45
 
46
46
  const externalState = reactive<Variant['state']>({})
@@ -73,6 +73,11 @@ export default _defineComponent({
73
73
  renderContext.currentVariant = props.variant
74
74
  renderContext.slotName = props.slotName
75
75
 
76
+ if (!sandbox.value) {
77
+ mounting = false
78
+ return
79
+ }
80
+
76
81
  host = createPreviewHost({
77
82
  name: 'RenderStorySubApp',
78
83
  el: sandbox.value,
@@ -36,7 +36,7 @@ export const RouterLinkStub = defineComponent({
36
36
  render() {
37
37
  const route = computed(() => defaultRoute)
38
38
  // mock reasonable return values to mimic vue-router's useLink
39
- const children = this.$slots?.default?.({
39
+ const children = this.$slots?.['default']?.({
40
40
  route,
41
41
  href: computed(() => route.value.href),
42
42
  isActive: computed(() => false),
@@ -1,8 +1,11 @@
1
1
  import type { Story } from '@poveste/shared'
2
2
  import type { PropType, VNode } from 'vue'
3
+ import type { PreviewRenderContext } from './render-context.js'
3
4
  import { omitInheritStoryProps } from '@poveste/shared'
4
- import { cloneVNode, computed, defineComponent, getCurrentInstance, h, isRef, provide, reactive, useAttrs } from 'vue'
5
+ import { cloneVNode, computed, defineComponent, h, isRef, reactive, useAttrs } from 'vue'
6
+ import { implicitStateContext, storyContext } from './context.js'
5
7
  import { useRenderContext } from './render-context.js'
8
+ import { isWritableBinding, useInstance } from './util.js'
6
9
  import Variant from './Variant.js'
7
10
 
8
11
  export default defineComponent({
@@ -25,7 +28,7 @@ export default defineComponent({
25
28
  },
26
29
 
27
30
  setup(props) {
28
- const vm = getCurrentInstance()
31
+ const vm = useInstance('Story')
29
32
 
30
33
  const attrs = useAttrs() as {
31
34
  story: Story
@@ -33,7 +36,7 @@ export default defineComponent({
33
36
 
34
37
  const renderContext = useRenderContext()
35
38
  const story = computed(() => attrs.story)
36
- provide('story', story)
39
+ storyContext.provide(story)
37
40
 
38
41
  const storyComponent: any = vm.parent
39
42
  const implicitState = reactive<Record<string, any>>({})
@@ -54,6 +57,10 @@ export default defineComponent({
54
57
  return
55
58
  }
56
59
 
60
+ if (!isWritableBinding(value)) {
61
+ return
62
+ }
63
+
57
64
  Object.defineProperty(implicitState, key, {
58
65
  enumerable: true,
59
66
  configurable: true,
@@ -88,7 +95,7 @@ export default defineComponent({
88
95
  for (const key in storyComponent.data) {
89
96
  addImplicitState(storyComponent.data, key)
90
97
  }
91
- provide('implicitState', () => implicitState)
98
+ implicitStateContext.provide(() => implicitState)
92
99
 
93
100
  function updateStory() {
94
101
  if (props.meta) {
@@ -100,7 +107,7 @@ export default defineComponent({
100
107
  }
101
108
 
102
109
  Object.assign(attrs.story, {
103
- slots: () => vm.proxy.$slots,
110
+ slots: () => vm.slots,
104
111
  })
105
112
  }
106
113
 
@@ -112,6 +119,32 @@ export default defineComponent({
112
119
  updateStory,
113
120
  }
114
121
 
122
+ /**
123
+ * What a `<Variant>` takes from its story: the attrs the story was given, then
124
+ * what the story carries, neither overriding what the variant set itself.
125
+ *
126
+ * Both render passes need it. `autoPropsDisabled` set on a story reached only
127
+ * the mount pass, so a story turning auto-props off still offered controls in
128
+ * the realm that renders it (#890).
129
+ */
130
+ function inheritedFromStory(vnode: VNode) {
131
+ const inherited: Record<string, any> = {}
132
+
133
+ for (const attr in vm.attrs) {
134
+ if (typeof vnode.props?.[attr] === 'undefined') {
135
+ inherited[attr] = vm.attrs[attr]
136
+ }
137
+ }
138
+
139
+ for (const attr in attrs.story) {
140
+ if (!omitInheritStoryProps.includes(attr) && typeof vnode.props?.[attr] === 'undefined') {
141
+ inherited[attr] = Reflect.get(attrs.story, attr)
142
+ }
143
+ }
144
+
145
+ return inherited
146
+ }
147
+
115
148
  function renderMountStory() {
116
149
  let index = 0
117
150
 
@@ -126,8 +159,9 @@ export default defineComponent({
126
159
  // that as it registers, so the gate held for the first render only and
127
160
  // turned the re-render a retarget needs into a full mount of every
128
161
  // sibling.
129
- const targetIndex = renderContext?.targetVariantId
130
- ? attrs.story.variants.findIndex(v => v.id === renderContext.targetVariantId)
162
+ const targetVariantId = renderContext?.targetVariantId
163
+ const targetIndex = targetVariantId
164
+ ? attrs.story.variants.findIndex(v => v.id === targetVariantId)
131
165
  : -1
132
166
 
133
167
  const applyAttrs = (vnodes: VNode[]) => {
@@ -159,21 +193,11 @@ export default defineComponent({
159
193
  nextProps.key = variant.id
160
194
  }
161
195
 
162
- if (!vnode.props?.initState && !vnode.props?.['init-state']) {
196
+ if (!vnode.props?.['initState'] && !vnode.props?.['init-state']) {
163
197
  nextProps.initState = props.initState
164
198
  }
165
199
 
166
- for (const attr in vm.proxy.$attrs) {
167
- if (typeof vnode.props?.[attr] === 'undefined') {
168
- nextProps[attr] = vm.proxy.$attrs[attr]
169
- }
170
- }
171
-
172
- for (const attr in attrs.story) {
173
- if (!omitInheritStoryProps.includes(attr) && typeof vnode.props?.[attr] === 'undefined') {
174
- nextProps[attr] = attrs.story[attr]
175
- }
176
- }
200
+ Object.assign(nextProps, inheritedFromStory(vnode))
177
201
 
178
202
  result.push(cloneVNode(vnode, nextProps))
179
203
  continue
@@ -189,38 +213,59 @@ export default defineComponent({
189
213
  return result
190
214
  }
191
215
 
192
- return applyAttrs(vm.proxy.$slots.default?.() ?? [])
216
+ return applyAttrs(vm.slots['default']?.() ?? [])
193
217
  }
194
218
 
195
- function renderPreviewStory() {
196
- renderContext.nextVariantIndex.value = 0
219
+ function countVariantVnodes(vnodes: VNode[]): number {
220
+ let count = 0
221
+ for (const vnode of vnodes) {
222
+ // @ts-expect-error custom option
223
+ if (vnode?.type?.__povesteType === 'variant') {
224
+ count++
225
+ }
226
+ else if (Array.isArray(vnode?.children)) {
227
+ count += countVariantVnodes(vnode.children as VNode[])
228
+ }
229
+ }
230
+ return count
231
+ }
232
+
233
+ function renderPreviewStory(context: PreviewRenderContext) {
234
+ context.nextVariantIndex.value = 0
197
235
 
198
236
  const slotProps = {
199
- state: renderContext.externalState,
237
+ state: context.externalState,
200
238
  }
201
239
 
202
- const children = []
240
+ const children: VNode[] = []
203
241
 
204
- if (renderContext.slotName === 'controls') {
205
- children.push(...(vm.proxy.$slots.controls?.(slotProps) ?? []))
242
+ if (context.slotName === 'controls') {
243
+ children.push(...(vm.slots['controls']?.(slotProps) ?? []))
206
244
  }
207
245
 
208
- if (renderContext.slotName === 'default' || attrs.story.meta?.hasVariantChildComponents) {
209
- children.push(...(vm.proxy.$slots.default?.(slotProps) ?? []))
246
+ if (context.slotName === 'default' || attrs.story.meta?.hasVariantChildComponents) {
247
+ children.push(...(vm.slots['default']?.(slotProps) ?? []))
210
248
  }
211
249
 
212
250
  // Same story as the mount pass above: every Variant component here runs
213
251
  // its full setup and then all but the current one render null (#197).
214
252
  // Keep only the current one, and seed the positional index so it still
215
253
  // resolves to the right variant data. Filtering only the default slot —
216
- // the controls slot carries controls, and stories with variants in child
217
- // components are invisible to a vnode walk.
254
+ // the controls slot carries controls.
255
+ //
256
+ // Gated on the walk seeing every variant, not on `hasVariantChildComponents`,
257
+ // for the reason the mount pass gives: every explicit <Variant> sets that flag
258
+ // as it renders, so from the second render on the filter never ran, and each
259
+ // retarget rendered all N Variant components to show one (#873). A story
260
+ // whose variants live in child components still renders them all, because
261
+ // its walk finds fewer than `variants.length`.
262
+ const currentVariant = context.currentVariant
218
263
  if (
219
- renderContext.slotName === 'default'
220
- && renderContext.currentVariant
221
- && !attrs.story.meta?.hasVariantChildComponents
264
+ context.slotName === 'default'
265
+ && currentVariant
266
+ && countVariantVnodes(children) === attrs.story.variants.length
222
267
  ) {
223
- const targetIndex = attrs.story.variants.findIndex(v => v.id === renderContext.currentVariant.id)
268
+ const targetIndex = attrs.story.variants.findIndex(v => v.id === currentVariant.id)
224
269
  if (targetIndex !== -1) {
225
270
  let seen = 0
226
271
  let kept = false
@@ -230,7 +275,7 @@ export default defineComponent({
230
275
  // @ts-expect-error custom option
231
276
  if (vnode?.type?.__povesteType === 'variant') {
232
277
  if (seen === targetIndex) {
233
- result.push(vnode)
278
+ result.push(cloneVNode(vnode, inheritedFromStory(vnode)))
234
279
  kept = true
235
280
  }
236
281
  seen++
@@ -245,7 +290,7 @@ export default defineComponent({
245
290
  }
246
291
  const filtered = keepTargetVariant(children)
247
292
  if (kept) {
248
- renderContext.nextVariantIndex.value = targetIndex
293
+ context.nextVariantIndex.value = targetIndex
249
294
  return filtered
250
295
  }
251
296
  }
@@ -263,14 +308,15 @@ export default defineComponent({
263
308
  if (firstVariant?.id === '_default') {
264
309
  return h(Variant, {
265
310
  variant: firstVariant,
266
- initState: this.initState,
311
+ ...this.initState ? { initState: this.initState } : {},
267
312
  implicit: true,
268
313
  ...this.$attrs,
269
314
  }, this.$slots)
270
315
  }
271
316
 
272
- if (this.renderContext?.mode === 'render') {
273
- return this.renderPreviewStory()
317
+ const renderContext = this.renderContext
318
+ if (renderContext?.mode === 'render') {
319
+ return this.renderPreviewStory(renderContext)
274
320
  }
275
321
 
276
322
  return this.renderMountStory()
@@ -1,10 +1,12 @@
1
1
  import type { Variant } from '@poveste/shared'
2
- import type { ComputedRef, PropType } from 'vue'
2
+ import type { PropType } from 'vue'
3
+ import type { PreviewRenderContext } from './render-context.js'
3
4
  import { applyState, autoPropsStateKeys } from '@poveste/shared'
4
- import { computed, defineComponent, getCurrentInstance, inject, onBeforeUnmount, useAttrs } from 'vue'
5
+ import { computed, defineComponent, onBeforeUnmount, useAttrs } from 'vue'
5
6
  import { syncVariantAutoProps } from './auto-props.js'
7
+ import { implicitStateContext, storyContext } from './context.js'
6
8
  import { useRenderContext } from './render-context.js'
7
- import { syncStateBundledAndExternal, toRawDeep } from './util.js'
9
+ import { syncStateBundledAndExternal, toRawDeep, useInstance } from './util.js'
8
10
 
9
11
  // const logLocation = location.href.includes('__sandbox') ? '[Sandbox]' : '[Host]'
10
12
 
@@ -54,14 +56,14 @@ export default defineComponent({
54
56
  const attrs = useAttrs() as {
55
57
  variant?: Variant
56
58
  }
57
- const vm = getCurrentInstance()
58
- const story = inject<ComputedRef<any>>('story')
59
- const implicitState = inject<() => any>('implicitState')
59
+ const vm = useInstance('Variant')
60
+ const story = storyContext.injectOptional()
61
+ const implicitState = implicitStateContext.injectOptional()
60
62
  const renderContext = useRenderContext()
61
63
  let lastPropsTypesSnapshot: string
62
- let renderVariant: Variant
63
- let renderStateSync: ReturnType<typeof syncStateBundledAndExternal>
64
- let mountStateSync: ReturnType<typeof syncStateBundledAndExternal>
64
+ let renderVariant: Variant | undefined
65
+ let renderStateSync: ReturnType<typeof syncStateBundledAndExternal> | null = null
66
+ let mountStateSync: ReturnType<typeof syncStateBundledAndExternal> | undefined
65
67
 
66
68
  // Registered before the first `await` below, and deliberately so. This is
67
69
  // an async setup(), and Vue drops any lifecycle hook registered after an
@@ -83,13 +85,17 @@ export default defineComponent({
83
85
  applyState(mountVariant.value.state, toRawDeep(state))
84
86
  }
85
87
 
86
- if (renderContext?.mode !== 'render' && mountVariant.value && implicitState) {
88
+ // `syncState` is false in a sandbox realm, where the render tree beside this
89
+ // one already syncs the same `variant.state` against the same story's
90
+ // bindings. Both were live, so an edit walked the graph twice over for one
91
+ // copy of it (#964).
92
+ if (renderContext?.mode !== 'render' && renderContext?.syncState !== false && mountVariant.value && implicitState) {
87
93
  mountStateSync = syncStateBundledAndExternal(mountVariant.value.state, implicitState(), autoPropsStateKeys)
88
94
  }
89
95
 
90
96
  function updateVariant(variant: Variant) {
91
97
  Object.assign(variant, {
92
- slots: () => vm.proxy.$slots,
98
+ slots: () => vm.slots,
93
99
  source: props.source,
94
100
  responsiveDisabled: props.responsiveDisabled,
95
101
  autoPropsDisabled: props.autoPropsDisabled,
@@ -98,12 +104,13 @@ export default defineComponent({
98
104
  configReady: true,
99
105
  })
100
106
 
101
- if (!props.implicit && !story.value?.meta?.hasVariantChildComponents) {
102
- if (!story.value.meta) {
103
- story.value.meta = {}
107
+ const storyValue = story?.value
108
+ if (storyValue && !props.implicit && !storyValue.meta?.hasVariantChildComponents) {
109
+ if (!storyValue.meta) {
110
+ storyValue.meta = {}
104
111
  }
105
112
 
106
- Object.assign(story.value.meta, {
113
+ Object.assign(storyValue.meta, {
107
114
  hasVariantChildComponents: true,
108
115
  })
109
116
  }
@@ -119,33 +126,33 @@ export default defineComponent({
119
126
  }
120
127
 
121
128
  if (!renderVariant) {
122
- renderVariant = story.value?.variants[renderContext.nextVariantIndex.value]
129
+ renderVariant = story?.value?.variants[renderContext.nextVariantIndex.value]
123
130
  renderContext.nextVariantIndex.value++
124
131
  }
125
132
 
126
133
  return renderVariant
127
134
  }
128
135
 
129
- function renderVariantSlot(variant: Variant) {
130
- if (renderContext.slotName === 'controls') {
131
- return vm.proxy.$slots.controls?.({
132
- state: renderContext.externalState,
136
+ function renderVariantSlot(variant: Variant, context: PreviewRenderContext) {
137
+ if (context.slotName === 'controls') {
138
+ return vm.slots['controls']?.({
139
+ state: context.externalState,
133
140
  }) ?? null
134
141
  }
135
142
 
136
- if (renderContext.slotName !== 'default') {
143
+ if (context.slotName !== 'default') {
137
144
  return null
138
145
  }
139
146
 
140
- const vnodes = vm.proxy.$slots.default?.({
141
- state: renderContext.externalState,
147
+ const vnodes = vm.slots['default']?.({
148
+ state: context.externalState,
142
149
  }) ?? null
143
150
 
144
151
  if (vnodes && !variant.autoPropsDisabled) {
145
152
  lastPropsTypesSnapshot = syncVariantAutoProps(
146
153
  variant,
147
154
  vnodes,
148
- renderContext.externalState,
155
+ context.externalState,
149
156
  lastPropsTypesSnapshot,
150
157
  )
151
158
  }
@@ -193,14 +200,15 @@ export default defineComponent({
193
200
  this.updateVariant(variant)
194
201
  this.syncRenderVariantState(variant)
195
202
 
196
- if (this.renderContext?.mode !== 'render') {
203
+ const renderContext = this.renderContext
204
+ if (renderContext?.mode !== 'render') {
197
205
  return null
198
206
  }
199
207
 
200
- if (this.renderContext.currentVariant?.id !== variant.id) {
208
+ if (renderContext.currentVariant?.id !== variant.id) {
201
209
  return null
202
210
  }
203
211
 
204
- return this.renderVariantSlot(variant)
212
+ return this.renderVariantSlot(variant, renderContext)
205
213
  },
206
214
  })