@poveste/plugin-svelte 0.6.1 → 0.7.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,191 +0,0 @@
1
- import type { Story, Variant } from '@poveste/shared'
2
- import type {
3
- PropType as _PropType,
4
- } from '@poveste/vendors/vue'
5
- import type { SvelteStorySetupApi, SvelteStorySetupHandler } from '../helpers.js'
6
- import { components } from '@poveste/controls'
7
- import {
8
- defineComponent as _defineComponent,
9
- h as _h,
10
- onMounted as _onMounted,
11
- onUnmounted as _onUnmounted,
12
- ref as _ref,
13
- watch as _watch,
14
- } from '@poveste/vendors/vue'
15
- // @ts-expect-error virtual module id
16
- import * as generatedSetup from 'virtual:$poveste-generated-global-setup'
17
- // @ts-expect-error virtual module id
18
- import * as setup from 'virtual:$poveste-setup'
19
- import {
20
- callSetupFunctions,
21
- createWrappedComponent,
22
- getLegacyStateApi,
23
- mountSvelteComponent,
24
- } from '../util/svelte.js'
25
- import RenderStorySvelte from './RenderStory.svelte'
26
- import RenderVariantSvelte from './RenderVariant.svelte'
27
- import { syncState } from './util'
28
- import Wrap from './Wrap.svelte'
29
-
30
- export default _defineComponent({
31
- name: 'RenderStory',
32
-
33
- props: {
34
- variant: {
35
- type: Object as _PropType<Variant>,
36
- required: true,
37
- },
38
-
39
- story: {
40
- type: Object as _PropType<Story>,
41
- required: true,
42
- },
43
-
44
- slotName: {
45
- type: String,
46
- default: 'default',
47
- },
48
- },
49
-
50
- setup(props, { emit }) {
51
- const el = _ref<HTMLDivElement>()
52
- let app: any
53
- let target: HTMLDivElement
54
-
55
- let tearDownHandlers: (() => void)[] = []
56
-
57
- function documentOn(event, cb) {
58
- document.addEventListener(event, cb)
59
- const off = () => document.removeEventListener(event, cb)
60
- tearDownHandlers.push(off)
61
- return {
62
- off,
63
- }
64
- }
65
-
66
- async function mountStory() {
67
- target = document.createElement('div')
68
- el.value.appendChild(target)
69
-
70
- let components = []
71
- const { off: registerComponentOff } = documentOn('SvelteRegisterComponent', (e) => {
72
- const { component } = e.detail
73
- components.push(component)
74
- })
75
-
76
- const mountedApp = await mountSvelteComponent(props.story.file.component, {
77
- target,
78
- props: {
79
- Hst: {
80
- Story: RenderStorySvelte,
81
- Variant: RenderVariantSvelte,
82
- ...getControls(),
83
- },
84
- },
85
- context: new Map(Object.entries({
86
- __pvtStory: props.story,
87
- __pvtVariant: props.variant,
88
- __pvtSlot: props.slotName,
89
- })),
90
- }, 'client')
91
- app = mountedApp.app
92
- tearDownHandlers.push(() => {
93
- mountedApp.destroy()
94
- })
95
-
96
- let appComponent = components.find(c => c.$$ && app?.$$ && c.$$ === app.$$) ?? app
97
- registerComponentOff()
98
- components = []
99
-
100
- function patchReplaceIfAvailable() {
101
- const origReplace = appComponent?.$replace
102
- if (!origReplace) {
103
- return
104
- }
105
-
106
- appComponent.$replace = (...args) => {
107
- const result = origReplace.apply(appComponent, args)
108
- appComponent = result ?? appComponent
109
- return result
110
- }
111
- }
112
- patchReplaceIfAvailable()
113
-
114
- const stateApi = getLegacyStateApi(appComponent)
115
- if (stateApi) {
116
- const { apply, stop } = syncState(props.variant.state, (value) => {
117
- stateApi.injectState(value)
118
- })
119
- tearDownHandlers.push(stop)
120
-
121
- let frameId: number
122
- const syncFromComponent = () => {
123
- apply(stateApi.captureState())
124
- frameId = requestAnimationFrame(syncFromComponent)
125
- }
126
-
127
- frameId = requestAnimationFrame(syncFromComponent)
128
- tearDownHandlers.push(() => {
129
- cancelAnimationFrame(frameId)
130
- })
131
-
132
- apply(stateApi.captureState())
133
- }
134
-
135
- const setupApi: SvelteStorySetupApi = {
136
- app,
137
- story: props.story,
138
- variant: props.variant,
139
- }
140
-
141
- await callSetupFunctions(generatedSetup, setup, setupApi, props.variant.setupApp as SvelteStorySetupHandler | null)
142
-
143
- emit('ready')
144
- }
145
-
146
- function unmountStory() {
147
- tearDownHandlers.forEach(fn => fn())
148
- tearDownHandlers = []
149
- if (target) {
150
- target.parentNode?.removeChild(target)
151
- target = null
152
- }
153
- app = null
154
- }
155
-
156
- _watch(() => props.story.id, async () => {
157
- unmountStory()
158
- await mountStory()
159
- })
160
-
161
- _onMounted(async () => {
162
- await mountStory()
163
- })
164
-
165
- _onUnmounted(() => {
166
- unmountStory()
167
- })
168
-
169
- return {
170
- el,
171
- }
172
- },
173
-
174
- render() {
175
- return _h('div', {
176
- ref: 'el',
177
- })
178
- },
179
- })
180
-
181
- function getControls() {
182
- const result: Record<string, any> = {}
183
- for (const key in components) {
184
- result[key.substring(3)] = wrapComponent(components[key])
185
- }
186
- return result
187
- }
188
-
189
- function wrapComponent(controlComponent) {
190
- return createWrappedComponent(Wrap, controlComponent)
191
- }
@@ -1,63 +0,0 @@
1
- import { applyState, clone } from '@poveste/shared'
2
- import { watch as _watch } from '@poveste/vendors/vue'
3
-
4
- function cleanupState(state: Record<string, any>): Record<string, any> {
5
- const result = {}
6
- for (const key in state) {
7
- if (key === 'Hst') continue
8
- const value = state[key]
9
- if (typeof value === 'function') continue
10
- if (typeof value === 'undefined') continue
11
- if (value instanceof HTMLElement) continue
12
- if (typeof value === 'object' && value?.$$) continue
13
- result[key] = value
14
- }
15
- return result
16
- }
17
-
18
- export function syncState(variantState, onChange: (state) => unknown) {
19
- // Same flag, same #95 caveat as `plugin-vue` and the sandbox bridge: `apply`
20
- // may only claim the next firing when its write will actually cause one, and
21
- // `applyState` reports that. It matters more here than anywhere else, because
22
- // the render loop calls `apply` on *every animation frame* whether or not the
23
- // story did anything — set the flag unconditionally and it is set through
24
- // roughly every other frame, ready to eat whatever lands in it.
25
- //
26
- // The `onChange` direction below still sets it blind. Nothing here can see
27
- // whether handing the state to the component changed anything, so the same
28
- // hazard remains on that side. It cannot be exercised today: `syncState` is
29
- // only reachable through `getLegacyStateApi`, which needs Svelte 4's
30
- // `$capture_state`/`$inject_state`, and the Svelte example is on 5 with both
31
- // state-sync specs already `fixme` under #81. Whoever revives that path
32
- // inherits this note.
33
- let syncing = false
34
-
35
- const _stop = _watch(() => variantState, (value) => {
36
- if (value == null) return
37
- if (syncing) {
38
- syncing = false
39
- return
40
- }
41
- syncing = true
42
- onChange(cleanupState(value))
43
- }, {
44
- deep: true,
45
- immediate: true,
46
- })
47
-
48
- function apply(value) {
49
- if (value == null) return
50
- if (syncing) {
51
- syncing = false
52
- return
53
- }
54
- syncing = applyState(variantState, clone(cleanupState(value)))
55
- }
56
-
57
- return {
58
- apply,
59
- stop() {
60
- _stop()
61
- },
62
- }
63
- }
@@ -1,41 +0,0 @@
1
- <script>
2
- import { getContext, setContext } from 'svelte'
3
-
4
- export let title = null
5
- export let id = null
6
- export let group = null
7
- export let layout = null
8
- export let icon = null
9
- export let iconColor = null
10
- export let docsOnly = false
11
- export let initState = null
12
-
13
- const addStory = getContext('__pvtAddStory')
14
- const file = getContext('__pvtStoryFile')
15
-
16
- const story = {
17
- id: id ?? file.id,
18
- title: title ?? file.fileName,
19
- group,
20
- layout,
21
- icon,
22
- iconColor,
23
- docsOnly,
24
- variants: [],
25
- }
26
-
27
- addStory(story)
28
-
29
- // Collection renders the story's markup purely to discover its variants, but
30
- // that markup now reads `state`. Without a value here every expression
31
- // referencing it throws and collection fails with a bare
32
- // `Cannot read properties of undefined` (#81).
33
- const state = initState ? initState() : {}
34
-
35
- setContext('__pvtStory', story)
36
- setContext('__pvtAddVariant', (variant) => {
37
- story.variants.push(variant)
38
- })
39
- </script>
40
-
41
- <slot {state} />
@@ -1,28 +0,0 @@
1
- <script>
2
- import { getContext } from 'svelte'
3
-
4
- export let title = 'untitled'
5
- export let id = null
6
- export let icon = null
7
- export let iconColor = null
8
- // Absorbs the prop; collection never renders variant children. `export const`
9
- // (the compiler's suggestion) would make passing one an error.
10
- // svelte-ignore export_let_unused
11
- export let initState = null
12
-
13
- const story = getContext('__pvtStory')
14
- const addVariant = getContext('__pvtAddVariant')
15
-
16
- function generateId() {
17
- return `${story.id}-${story.variants.length}`
18
- }
19
-
20
- const variant = {
21
- id: id ?? generateId(),
22
- title,
23
- icon,
24
- iconColor,
25
- }
26
-
27
- addVariant(variant)
28
- </script>
@@ -1,65 +0,0 @@
1
- import type { ServerRunPayload } from '@poveste/shared'
2
- import type { SvelteStorySetupApi } from '../helpers.js'
3
- import { tick } from 'svelte'
4
- // @ts-expect-error virtual module id
5
- import * as generatedSetup from 'virtual:$poveste-generated-global-setup'
6
- // @ts-expect-error virtual module id
7
- import * as setup from 'virtual:$poveste-setup'
8
- import {
9
- callSetupFunctions,
10
- mountSvelteComponent,
11
- } from '../util/svelte.js'
12
- import Story from './Story.svelte'
13
- import Variant from './Variant.svelte'
14
-
15
- export async function run({ file, el, storyData }: ServerRunPayload) {
16
- const { default: Comp } = await import(/* @vite-ignore */ file.moduleId)
17
-
18
- const mountedApp = await mountSvelteComponent(Comp, {
19
- target: el,
20
- props: {
21
- Hst: {
22
- Story,
23
- Variant,
24
- },
25
- },
26
- context: new Map(Object.entries({
27
- __pvtAddStory(data) {
28
- storyData.push(data)
29
- },
30
- __pvtStoryFile: file,
31
- })),
32
- }, 'client')
33
- const app = mountedApp.app
34
-
35
- const setupApi: SvelteStorySetupApi = {
36
- app,
37
- story: null,
38
- variant: null,
39
- }
40
-
41
- await callSetupFunctions(generatedSetup, setup, setupApi)
42
-
43
- await tick()
44
-
45
- // A file with no `Hst.Story` in it collects nothing, which is a thing people
46
- // write — a scratch file, a component that lost its story tag in a refactor.
47
- // The optional chain below used to guard only the read: with no story the
48
- // condition came out true and the body then dereferenced the same `undefined`,
49
- // so the collector crashed the whole build instead of skipping one file. The
50
- // Vue collector warns and carries on, and the shared warning downstream
51
- // (`No story found for …`) is already written for exactly this.
52
- if (!storyData[0]) {
53
- mountedApp.destroy()
54
- return
55
- }
56
-
57
- if (!storyData[0].variants.length) {
58
- storyData[0].variants.push({
59
- id: '_default',
60
- title: 'default',
61
- })
62
- }
63
-
64
- mountedApp.destroy()
65
- }
@@ -1,29 +0,0 @@
1
- import type { ClientCommandOptions } from 'poveste'
2
- import { kebabCase } from 'change-case'
3
- import { openStory, sendEvent } from 'poveste/plugin'
4
-
5
- export default {
6
- prompts: [
7
- {
8
- field: 'component',
9
- label: 'Choose a component',
10
- type: 'select',
11
- options: async search => sendEvent('listSvelteComponents', { search }),
12
- required: true,
13
- },
14
- {
15
- field: 'fileName',
16
- label: 'File name',
17
- type: 'text',
18
- required: true,
19
- defaultValue: answers => answers.component?.replace(/[^/]+\/([^/]+)\.svelte$/, '$1.story.svelte'),
20
- },
21
- ],
22
- clientAction: (params) => {
23
- const index = params.component.lastIndexOf('/')
24
- const dirname = params.component.substring(0, index + 1)
25
- const file = `${dirname}${params.fileName}`
26
- const storyId = kebabCase(file.toLowerCase())
27
- openStory(storyId)
28
- },
29
- } as ClientCommandOptions
@@ -1,62 +0,0 @@
1
- import type { PluginCommand } from 'poveste'
2
- import fs from 'node:fs'
3
- import launchEditor from 'launch-editor'
4
- import path from 'pathe'
5
-
6
- export default {
7
- id: 'poveste:plugin-svelte:generate-story',
8
- label: 'Generate Svelte story from component',
9
- icon: 'https://svelte.dev/favicon.png',
10
- searchText: 'generate create',
11
- async serverAction(params) {
12
- const targetFile = path.join(path.dirname(params.component), params.fileName)
13
-
14
- if (fs.existsSync(targetFile)) {
15
- throw new Error(`File ${targetFile} already exists`)
16
- }
17
-
18
- const { component, componentName, isTs } = await getComponentInfo(params.component)
19
-
20
- const content = `<script${isTs ? ' lang="ts"' : ''}>
21
- ${isTs
22
- ? `import type { Hst } from '@poveste/plugin-svelte'\n
23
- `
24
- : ''}import ${componentName} from './${component}'
25
-
26
- export let Hst${isTs ? ': Hst' : ''}
27
- </script>
28
-
29
- <Hst.Story>
30
- <Hst.Variant title="Default">
31
- <${componentName} />
32
-
33
- <svelte:fragment slot="controls">
34
- <!-- Put controls here -->
35
- </svelte:fragment>
36
- </Hst.Variant>
37
- </Hst.Story>
38
- `
39
-
40
- await fs.promises.writeFile(targetFile, content, 'utf-8')
41
-
42
- launchEditor(targetFile)
43
- },
44
- clientSetupFile: '@poveste/plugin-svelte/dist/commands/generate-story.client.js',
45
- } as PluginCommand
46
-
47
- async function isComponentTs(component: string) {
48
- const componentContent = await fs.promises.readFile(component, 'utf-8')
49
- return componentContent.includes('lang="ts"')
50
- }
51
-
52
- async function getComponentInfo(file: string) {
53
- const component = path.basename(file)
54
- const componentName = component.replace(path.extname(component), '')
55
- const isTs = await isComponentTs(file)
56
-
57
- return {
58
- component,
59
- componentName,
60
- isTs,
61
- }
62
- }
package/src/helpers.ts DELETED
@@ -1,131 +0,0 @@
1
- import type { HstControlOption } from '@poveste/controls'
2
- import type { Story, StoryProps, Variant, VariantProps } from '@poveste/shared'
3
- import type { Component, Snippet } from 'svelte'
4
-
5
- export interface SvelteStorySetupApi {
6
- app: any
7
- story?: Story
8
- variant?: Variant
9
- }
10
-
11
- export type SvelteStorySetupHandler = (api: SvelteStorySetupApi) => Promise<void> | void
12
-
13
- export function defineSetupSvelte(handler: SvelteStorySetupHandler): SvelteStorySetupHandler {
14
- return handler
15
- }
16
-
17
- export const defineSetupSvelte3 = defineSetupSvelte
18
- export const defineSetupSvelte4 = defineSetupSvelte
19
- export const defineSetupSvelte5 = defineSetupSvelte
20
-
21
- export type StoryState = Record<string, any>
22
-
23
- /**
24
- * Props plus Svelte 5 children.
25
- *
26
- * `StoryProps` and `VariantProps` live in `@poveste/shared` and are shared with
27
- * the Vue plugin, so a Svelte-only `Snippet` cannot be added there — it is
28
- * intersected in here instead.
29
- *
30
- * This is what the move off `SvelteComponentTyped` needed. The class form carried
31
- * Svelte 4 slot typing, so nested content type-checked implicitly; `Component<P>`
32
- * has no slots, and Svelte 5 passes children as a prop (#99).
33
- */
34
- type WithChildren<P> = P & {
35
- children?: Snippet<[{ state: StoryState }]>
36
- controls?: Snippet<[{ state: StoryState }]>
37
- }
38
-
39
- /**
40
- * Story state is owned by poveste, not by the story component.
41
- *
42
- * The story is mounted once per slot, so a component-local variable exists twice
43
- * and cannot be shared. `initState` seeds `variant.state`, which both mounts read
44
- * and which the sandbox bridge carries across the iframe boundary (#81).
45
- */
46
- type WithState<P> = Omit<P, 'source'> & {
47
- initState?: () => StoryState
48
- /** A literal, or a function of the variant state so the source panel can track the controls. */
49
- source?: string | ((state: StoryState) => string)
50
- }
51
-
52
- export interface Hst {
53
- // Main built-ins
54
- Story: Component<WithState<WithChildren<StoryProps>>>
55
- Variant: Component<WithState<WithChildren<VariantProps>>>
56
- // Controls
57
- // Deliberately permissive. This was previously a bare `SvelteComponentTyped`,
58
- // i.e. props `any`; it wraps a Vue control whose default slot renders the label,
59
- // and the prop set `Wrap` forwards is not pinned down. Narrowing it is its own
60
- // change rather than a silent break.
61
- Button: Component<WithChildren<Record<string, any>>>
62
- ButtonGroup: Component<{
63
- value?: string
64
- options: (string | HstControlOption)[]
65
- title?: string
66
- }>
67
- Checkbox: Component<{
68
- value?: boolean
69
- title: string
70
- }>
71
- CheckboxList: Component<{
72
- value: string[]
73
- options: (string | HstControlOption)[]
74
- title?: string
75
- }>
76
- Text: Component<{
77
- value?: string
78
- title: string
79
- }>
80
- Number: Component<{
81
- value?: number
82
- title: string
83
- step?: number
84
- }>
85
- Slider: Component<{
86
- value?: number
87
- title: string
88
- min: number
89
- max: number
90
- step?: number
91
- }>
92
- Textarea: Component<{
93
- value?: string
94
- title: string
95
- }>
96
- Select: Component<{
97
- value?: string
98
- title: string
99
- options: Record<string, any> | string[] | HstControlOption[]
100
- }>
101
- Radio: Component<{
102
- value?: string
103
- options: HstControlOption[]
104
- title?: string
105
- }>
106
- Json: Component<{
107
- value: unknown
108
- title: string
109
- }>
110
- Shades: Component<{
111
- shades: Record<string, any>
112
- getName?: (key: string, color: string) => string
113
- search?: string
114
- }>
115
- TokenList: Component<{
116
- tokens: Record<string, string | number | any[] | Record<string, any>>
117
- getName?: (key: string, value: string | number | any[] | Record<string, any>) => string
118
- }>
119
- TokenGrid: Component<{
120
- tokens: Record<string, string | number | any[] | Record<string, any>>
121
- getName?: (key: string, value: string | number | any[] | Record<string, any>) => string
122
- colSize?: number
123
- }>
124
- CopyIcon: Component<{
125
- content: string
126
- }>
127
- ColorSelect: Component<{
128
- value?: string
129
- title: string
130
- }>
131
- }