@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.
- package/README.md +2 -2
- package/components.d.ts +4 -0
- package/dist/bundled/client/app/MountStory.js +8 -3
- package/dist/bundled/client/app/RenderStory.js +15 -9
- package/dist/bundled/client/app/Story.js +76 -62
- package/dist/bundled/client/app/Variant.js +48 -42
- package/dist/bundled/client/app/context.js +5 -0
- package/dist/bundled/client/app/global-components.js +11 -10
- package/dist/bundled/client/app/host.js +2 -2
- package/dist/bundled/client/app/render-context.js +7 -7
- package/dist/bundled/client/app/util.js +36 -26
- package/dist/bundled/client/client.js +4 -4
- package/dist/bundled/client/codegen.js +79 -60
- package/dist/bundled/client/context.js +20 -0
- package/dist/bundled/client/server/Story.js +28 -25
- package/dist/bundled/client/server/Variant.js +13 -16
- package/dist/bundled/client/server/context.js +5 -0
- package/dist/bundled/client/server/run.js +29 -29
- package/dist/client/app/MountStory.d.ts +12 -3
- package/dist/client/app/RenderStory.d.ts +1 -1
- package/dist/client/app/Story.d.ts +10 -7
- package/dist/client/app/Variant.d.ts +14 -13
- package/dist/client/app/context.d.ts +9 -0
- package/dist/client/app/render-context.d.ts +16 -1
- package/dist/client/app/util.d.ts +38 -3
- package/dist/client/codegen.d.ts +29 -0
- package/dist/client/context.d.ts +40 -0
- package/dist/client/server/Story.d.ts +17 -17
- package/dist/client/server/Variant.d.ts +10 -10
- package/dist/client/server/context.d.ts +7 -0
- package/dist/commands/generate-story.client.js +4 -4
- package/dist/commands/generate-story.server.js +2 -2
- package/dist/helpers.d.ts +2 -2
- package/dist/index.node.js +2 -0
- package/dist/util/list-components.d.ts +1 -1
- package/dist/util/list-components.js +63 -6
- package/dist/util/list-components.spec.d.ts +1 -0
- package/dist/util/list-components.spec.js +46 -0
- package/package.json +17 -16
- package/src/client/__tests__/codegen-attrs.spec.ts +149 -0
- package/src/client/__tests__/context.spec.ts +50 -0
- package/src/client/app/MountStory.ts +11 -1
- package/src/client/app/RenderStory.ts +6 -1
- package/src/client/app/RouterLinkStub.ts +1 -1
- package/src/client/app/Story.ts +85 -39
- package/src/client/app/Variant.ts +35 -27
- package/src/client/app/__tests__/preview-variants.spec.ts +76 -0
- package/src/client/app/__tests__/state-sync.spec.ts +113 -2
- package/src/client/app/__tests__/writable-binding.spec.ts +39 -0
- package/src/client/app/auto-props.ts +2 -2
- package/src/client/app/context.ts +17 -0
- package/src/client/app/global-components.ts +20 -14
- package/src/client/app/host.ts +9 -6
- package/src/client/app/render-context.ts +25 -5
- package/src/client/app/util.ts +104 -9
- package/src/client/codegen.ts +81 -29
- package/src/client/context.ts +66 -0
- package/src/client/server/Story.ts +16 -10
- package/src/client/server/Variant.ts +13 -14
- package/src/client/server/__tests__/nesting-errors.spec.ts +48 -0
- package/src/client/server/context.ts +18 -0
- package/src/client/server/run.ts +11 -7
- package/src/commands/generate-story.client.ts +4 -4
- package/src/commands/generate-story.server.ts +2 -2
- package/src/helpers.ts +2 -2
- package/src/index.node.ts +2 -0
- package/src/util/list-components.spec.ts +54 -0
- 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),
|
package/src/client/app/Story.ts
CHANGED
|
@@ -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,
|
|
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 =
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
130
|
-
|
|
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
|
-
|
|
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.
|
|
216
|
+
return applyAttrs(vm.slots['default']?.() ?? [])
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
function
|
|
196
|
-
|
|
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:
|
|
237
|
+
state: context.externalState,
|
|
200
238
|
}
|
|
201
239
|
|
|
202
|
-
const children = []
|
|
240
|
+
const children: VNode[] = []
|
|
203
241
|
|
|
204
|
-
if (
|
|
205
|
-
children.push(...(vm.
|
|
242
|
+
if (context.slotName === 'controls') {
|
|
243
|
+
children.push(...(vm.slots['controls']?.(slotProps) ?? []))
|
|
206
244
|
}
|
|
207
245
|
|
|
208
|
-
if (
|
|
209
|
-
children.push(...(vm.
|
|
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
|
|
217
|
-
//
|
|
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
|
-
|
|
220
|
-
&&
|
|
221
|
-
&&
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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 {
|
|
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,
|
|
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 =
|
|
58
|
-
const story =
|
|
59
|
-
const 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
|
-
|
|
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.
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
131
|
-
return vm.
|
|
132
|
-
state:
|
|
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 (
|
|
143
|
+
if (context.slotName !== 'default') {
|
|
137
144
|
return null
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
const vnodes = vm.
|
|
141
|
-
state:
|
|
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
|
-
|
|
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
|
-
|
|
203
|
+
const renderContext = this.renderContext
|
|
204
|
+
if (renderContext?.mode !== 'render') {
|
|
197
205
|
return null
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
if (
|
|
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
|
})
|