@poveste/plugin-svelte 0.11.0 → 0.12.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.
@@ -0,0 +1,2 @@
1
+ // https://github.com/microsoft/TypeScript/issues/33079
2
+ export * from './dist/client/auto-props'
@@ -1 +1 @@
1
- import{watch as e}from"@poveste/vendors/vue";import{getContext as t,onDestroy as n}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as r from"svelte/internal/client";var i=r.from_html(`<!> <!>`,1);function a(a,o){let s=r.sanitize_slots(o);r.push(o,!1);let c=t(`__pvtStory`),l=r.mutable_source(t(`__pvtVariant`)),u=t(`__pvtSlot`),d=t(`__pvtIndex`),f=c.variants[d.value];d.value++;let p=r.get(l).id===f.id,m=r.prop(o,`source`,8,null),h=r.prop(o,`initState`,8,null);h()&&p&&!r.get(l).__pvtStateSeeded&&(r.mutate(l,r.get(l).__pvtStateSeeded=!0),r.mutate(l,r.get(l).state={...r.get(l).state,...h()()})),!h()&&p&&u===`controls`&&s.controls&&!r.get(l).__pvtStateWarned&&(r.mutate(l,r.get(l).__pvtStateWarned=!0),console.error([`[poveste] Variant "${f?.title??f?.id}" has a controls slot but no \`initState\`.`,`Controls will render and appear to work, but their edits cannot reach the story:`,`the story is mounted separately for each slot, so a component-local variable is not shared.`,"Pass `initState` and read state from the snippet —",`https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate`].join(` `)));let g=r.mutable_source(r.get(l)?.state),_=()=>{};p&&(_=e(()=>r.get(l)?.state,()=>{r.set(g,r.get(l)?.state)},{deep:!0})),n(()=>_()),r.legacy_pre_effect(()=>(r.deep_read_state(m()),r.get(g),r.get(l)),()=>{if(p){let e=typeof m()==`function`?m()(r.get(g)):m();e!=null&&Object.assign(r.get(l),{source:e})}}),r.legacy_pre_effect_reset(),r.init();var v=r.comment(),y=r.first_child(v),b=e=>{var t=i(),n=r.first_child(t),a=e=>{var t=r.comment(),n=r.first_child(t);r.slot(n,o,`default`,{get state(){return r.get(g)}},null),r.append(e,t)};r.if(n,e=>{u==="default"&&e(a)});var s=r.sibling(n,2),c=e=>{var t=r.comment(),n=r.first_child(t);r.slot(n,o,`controls`,{get state(){return r.get(g)}},null),r.append(e,t)};r.if(s,e=>{u===`controls`&&e(c)}),r.append(e,t)};r.if(y,e=>{p&&e(b)}),r.append(a,v),r.pop()}export{a as default};
1
+ import{shouldWarnAboutMissingInitState as e}from"../util/init-state-warning.js";import{watch as t}from"@poveste/vendors/vue";import{getContext as n,onDestroy as r}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as i from"svelte/internal/client";var a=i.from_html(`<!> <!>`,1);function o(o,s){let c=i.sanitize_slots(s);i.push(s,!1);let l=n(`__pvtStory`),u=i.mutable_source(n(`__pvtVariant`)),d=n(`__pvtSlot`),f=n(`__pvtIndex`),p=l.variants[f.value];f.value++;let m=i.get(u).id===p.id,h=i.prop(s,`source`,8,null),g=i.prop(s,`initState`,8,null);g()&&m&&!i.get(u).__pvtStateSeeded&&(i.mutate(u,i.get(u).__pvtStateSeeded=!0),i.mutate(u,i.get(u).state={...i.get(u).state,...g()()})),m&&e({hasInitState:!!g(),seeded:!!i.get(u).__pvtStateSeeded,slotName:d,hasControlsSlot:!!c.controls,alreadyWarned:!!i.get(u).__pvtStateWarned})&&(i.mutate(u,i.get(u).__pvtStateWarned=!0),console.error([`[poveste] Variant "${p?.title??p?.id}" has a controls slot but no \`initState\`.`,`Controls will render and appear to work, but their edits cannot reach the story:`,`the story is mounted separately for each slot, so a component-local variable is not shared.`,"Pass `initState` and read state from the snippet —",`https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate`].join(` `)));let _=i.mutable_source(i.get(u)?.state),v=()=>{};m&&(v=t(()=>i.get(u)?.state,()=>{i.set(_,i.get(u)?.state)},{deep:!0})),r(()=>v()),i.legacy_pre_effect(()=>(i.deep_read_state(h()),i.get(_),i.get(u)),()=>{if(m){let e=typeof h()==`function`?h()(i.get(_)):h();e!=null&&Object.assign(i.get(u),{source:e})}}),i.legacy_pre_effect_reset(),i.init();var y=i.comment(),b=i.first_child(y),x=e=>{var t=a(),n=i.first_child(t),r=e=>{var t=i.comment(),n=i.first_child(t);i.slot(n,s,`default`,{get state(){return i.get(_)}},null),i.append(e,t)};i.if(n,e=>{d==="default"&&e(r)});var o=i.sibling(n,2),c=e=>{var t=i.comment(),n=i.first_child(t);i.slot(n,s,`controls`,{get state(){return i.get(_)}},null),i.append(e,t)};i.if(o,e=>{d===`controls`&&e(c)}),i.append(e,t)};i.if(b,e=>{m&&e(x)}),i.append(o,y),i.pop()}export{o as default};
@@ -1,6 +1,7 @@
1
1
  <script>
2
2
  import { watch as _watch } from '@poveste/vendors/vue'
3
3
  import { getContext, onDestroy } from 'svelte'
4
+ import { shouldWarnAboutMissingInitState } from '../util/init-state-warning.js'
4
5
 
5
6
  const story = getContext('__pvtStory')
6
7
  const currentVariant = getContext('__pvtVariant')
@@ -27,9 +28,13 @@
27
28
  currentVariant.state = { ...currentVariant.state, ...initState() }
28
29
  }
29
30
 
30
- // Controls mount only — see the note in RenderStory: the two mounts do not share
31
- // a realm for iframe layouts, so this is what keeps it to one warning.
32
- if (!initState && shouldRender && slotName === 'controls' && $$slots.controls && !currentVariant.__pvtStateWarned) {
31
+ if (shouldRender && shouldWarnAboutMissingInitState({
32
+ hasInitState: Boolean(initState),
33
+ seeded: Boolean(currentVariant.__pvtStateSeeded),
34
+ slotName,
35
+ hasControlsSlot: Boolean($$slots.controls),
36
+ alreadyWarned: Boolean(currentVariant.__pvtStateWarned),
37
+ })) {
33
38
  currentVariant.__pvtStateWarned = true
34
39
  console.error([
35
40
  `[poveste] Variant "${variant?.title ?? variant?.id}" has a controls slot but no \`initState\`.`,
@@ -0,0 +1,14 @@
1
+ import type { AutoPropComponentDefinition } from '@poveste/shared';
2
+ type Values = Record<string, any>[][];
3
+ /**
4
+ * Publishes what the panel renders (`_hPropDefs`) and returns a store of what
5
+ * the reader has set, which the rewritten markup spreads onto each component.
6
+ */
7
+ export declare function autoProps(defs: AutoPropComponentDefinition[][]): import("svelte/store").Readable<Values>;
8
+ /**
9
+ * Which variant's controls this realm answers to, or -1 for none. The story's
10
+ * own `autoPropsDisabled` is read directly, because story props reach only an
11
+ * implicit variant (#466).
12
+ */
13
+ export declare function variantToDrive(story: any, variant: any): number;
14
+ export {};
@@ -0,0 +1 @@
1
+ import{watch as e}from"@poveste/vendors/vue";import{readable as t}from"svelte/store";import{applyState as n}from"@poveste/shared";import{getContext as r}from"svelte";var i=Object.freeze({});function a(a){let l=r(`__pvtStory`),u=r(`__pvtVariant`),d=c(l,u),f=a.map(e=>Array.from({length:s(e)}).fill(i));if(d<0)return o(u),t(f);let p=a[d]??[],m=()=>{let e=f.slice();return e[d]=Array.from({length:s(p)},(e,t)=>u.state?._hPropState?.[t]??i),e},h=null;function g(){!u.state||h===u.state&&u.state._hPropDefs?.length||(h=u.state,n(u.state,{_hPropDefs:p}),u.state._hPropState||n(u.state,{_hPropState:{}}))}return t(m(),t=>{let n=()=>{g(),t(m())};n();let r=[e(()=>u.state,n),e(()=>u.state?._hPropState,n,{deep:!0}),e(()=>u.state?._hPropDefs?.length,n)];return()=>r.forEach(e=>e())})}function o(e){e?.state?._hPropDefs?.length&&n(e.state,{_hPropDefs:[]})}function s(e){return e.reduce((e,t)=>Math.max(e,t.index+1),0)}function c(e,t){return!e||!t||e.autoPropsDisabled||t.autoPropsDisabled?-1:e.variants?.findIndex(e=>e.id===t.id)??-1}export{a as autoProps,c as variantToDrive};
@@ -1 +1 @@
1
- import{getContext as e,setContext as t}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as n from"svelte/internal/client";function r(r,i){n.push(i,!1);let a=n.prop(i,`title`,8,null),o=n.prop(i,`id`,8,null),s=n.prop(i,`group`,8,null),c=n.prop(i,`layout`,8,null),l=n.prop(i,`icon`,8,null),u=n.prop(i,`iconColor`,8,null),d=n.prop(i,`docsOnly`,8,!1),f=n.prop(i,`initState`,8,null),p=e(`__pvtAddStory`),m=e(`__pvtStoryFile`),h={id:o()??m.id,title:a()??m.fileName,group:s(),layout:c(),icon:l(),iconColor:u(),docsOnly:d(),variants:[]};p(h);let g=f()?f()():{};t(`__pvtStory`,h),t(`__pvtAddVariant`,e=>{h.variants.push(e)}),n.init();var _=n.comment(),v=n.first_child(_);n.slot(v,i,`default`,{get state(){return g}},null),n.append(r,_),n.pop()}export{r as default};
1
+ import{getContext as e,setContext as t}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as n from"svelte/internal/client";function r(r,i){n.push(i,!1);let a=n.prop(i,`title`,8,null),o=n.prop(i,`id`,8,null),s=n.prop(i,`group`,8,null),c=n.prop(i,`layout`,8,null),l=n.prop(i,`icon`,8,null),u=n.prop(i,`iconColor`,8,null),d=n.prop(i,`docsOnly`,8,!1),f=n.prop(i,`autoPropsDisabled`,8,null),p=n.prop(i,`initState`,8,null),m=e(`__pvtAddStory`),h=e(`__pvtStoryFile`),g={id:o()??h.id,title:a()??h.fileName,group:s(),layout:c(),icon:l(),iconColor:u(),docsOnly:d(),autoPropsDisabled:f(),variants:[]};m(g);let _=p()?p()():{};t(`__pvtStory`,g),t(`__pvtAddVariant`,e=>{g.variants.push(e)}),n.init();var v=n.comment(),y=n.first_child(v);n.slot(y,i,`default`,{get state(){return _}},null),n.append(r,v),n.pop()}export{r as default};
@@ -8,6 +8,7 @@
8
8
  export let icon = null
9
9
  export let iconColor = null
10
10
  export let docsOnly = false
11
+ export let autoPropsDisabled = null
11
12
  export let initState = null
12
13
 
13
14
  const addStory = getContext('__pvtAddStory')
@@ -21,6 +22,10 @@
21
22
  icon,
22
23
  iconColor,
23
24
  docsOnly,
25
+ // Collected rather than inherited down: `MountStory` forwards story props
26
+ // only to an implicit variant (#466). Left undefined when unset, or the
27
+ // collector's `??=` merge could never apply `defaultStoryProps`.
28
+ autoPropsDisabled,
24
29
  variants: [],
25
30
  }
26
31
 
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import { dirname, join } from 'pathe';
4
4
  import { defaultColors } from 'poveste';
5
5
  import generateStoryCommand from './commands/generate-story.server.js';
6
+ import { svelteAutoProps } from './util/auto-props-vite.js';
6
7
  import { svelteKitAssetsDir } from './util/kit-assets.js';
7
8
  import { listComponentFiles } from './util/list-components.js';
8
9
  import { disableStoryComponentHmr } from './util/story-hmr.js';
@@ -35,6 +36,7 @@ export function HstSvelte() {
35
36
  ],
36
37
  vite: {
37
38
  plugins: [
39
+ svelteAutoProps(),
38
40
  disableStoryComponentHmr(),
39
41
  ],
40
42
  ...svelteClientAliases.length ? { resolve: { alias: svelteClientAliases } } : {},
@@ -0,0 +1,9 @@
1
+ /** Runs before `vite-plugin-svelte`: after it compiles there is nothing left to rewrite (#233). */
2
+ export declare function svelteAutoProps(): {
3
+ name: string;
4
+ enforce: "pre";
5
+ transform(this: any, code: string, id: string): Promise<{
6
+ code: string;
7
+ map: any;
8
+ }>;
9
+ };
@@ -0,0 +1,38 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { transformStoryAutoProps } from './auto-props.js';
3
+ import { extractPropDefs } from './props.js';
4
+ // Cheaper than a parse, and unlike a filename pattern it cannot go blind when a
5
+ // project narrows `storyMatch`.
6
+ const RENDERS_A_STORY = '<Hst.Story';
7
+ const IMPORTS_A_COMPONENT = /from\s*['"][^'"]*\.svelte['"]/;
8
+ /** Runs before `vite-plugin-svelte`: after it compiles there is nothing left to rewrite (#233). */
9
+ export function svelteAutoProps() {
10
+ return {
11
+ name: 'poveste:svelte-auto-props',
12
+ enforce: 'pre',
13
+ async transform(code, id) {
14
+ if (!id.split('?')[0].endsWith('.svelte') || !code.includes(RENDERS_A_STORY) || !IMPORTS_A_COMPONENT.test(code)) {
15
+ return undefined;
16
+ }
17
+ const result = await transformStoryAutoProps(code, async (specifier) => {
18
+ const resolved = await this.resolve(specifier, id);
19
+ if (!resolved) {
20
+ return undefined;
21
+ }
22
+ const file = resolved.id.split('?')[0];
23
+ // Baked into this module, so Vite has to know it depends on the source.
24
+ this.addWatchFile(file);
25
+ try {
26
+ return extractPropDefs(await readFile(file, 'utf8'));
27
+ }
28
+ catch (error) {
29
+ // Not a failed build, but silence would look like a component that
30
+ // declares nothing.
31
+ this.warn(`[poveste] could not read props from ${file}: ${error.message}`);
32
+ return undefined;
33
+ }
34
+ });
35
+ return result === undefined ? undefined : { code: result, map: null };
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,9 @@
1
+ import type { PropDefinition } from '@poveste/shared';
2
+ /** Resolves an import specifier from the story file to that component's props. */
3
+ export type PropsOf = (specifier: string) => Promise<PropDefinition[] | undefined>;
4
+ export declare function parseable(code: string): string;
5
+ /**
6
+ * Every component a variant renders takes a spread of the values its controls
7
+ * hold; the props on offer come from the component's own source (#233).
8
+ */
9
+ export declare function transformStoryAutoProps(code: string, propsOf: PropsOf): Promise<string | undefined>;
@@ -0,0 +1,230 @@
1
+ import { parse } from 'svelte/compiler';
2
+ const RUNTIME = '@poveste/plugin-svelte/auto-props';
3
+ const HOLDER = '__pvtAutoProps';
4
+ const TABLE = '__pvtAutoPropDefs';
5
+ const HST = 'Hst';
6
+ // Line-anchored and newline-free, or a match runs past its own line and blanks
7
+ // the import below it. A trailing line comment is tolerated for the same reason.
8
+ const TYPE_IMPORT = /^[ \t]*import\s+type\b[^;\n]+?\bfrom[ \t]*(['"])[^'"\n]*\1(?:[ \t]*;)?[ \t]*(?:\/\/[^\n]*)?$/gm;
9
+ const IMPORT_BRACES = /\bimport\s*\{[^}]*\}/g;
10
+ const INLINE_TYPE = /\btype\s+[A-Za-z_$][\w$]*(?:\s+as\s+[A-Za-z_$][\w$]*)?\s*,?/g;
11
+ // Blanked rather than removed, so every offset still points at the same
12
+ // character. `import type { Hst }` beside `export let Hst: Hst` reads as a
13
+ // duplicate declaration.
14
+ export function parseable(code) {
15
+ return code
16
+ .replace(TYPE_IMPORT, blank)
17
+ .replace(IMPORT_BRACES, braces => braces.replace(INLINE_TYPE, blank));
18
+ }
19
+ function blank(match) {
20
+ return ' '.repeat(match.length);
21
+ }
22
+ /**
23
+ * Every component a variant renders takes a spread of the values its controls
24
+ * hold; the props on offer come from the component's own source (#233).
25
+ */
26
+ export async function transformStoryAutoProps(code, propsOf) {
27
+ // Applied twice, the second preamble redeclares the first.
28
+ if (code.includes(RUNTIME)) {
29
+ return undefined;
30
+ }
31
+ let ast;
32
+ try {
33
+ ast = parse(parseable(code), { modern: true });
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ const instance = ast.instance;
39
+ if (!instance) {
40
+ return undefined;
41
+ }
42
+ const imported = importedComponents(instance.content?.body ?? []);
43
+ if (imported.size === 0) {
44
+ return undefined;
45
+ }
46
+ const targets = collectTargets(ast.fragment, imported);
47
+ if (targets === undefined || targets.length === 0) {
48
+ return undefined;
49
+ }
50
+ const defs = await describe(targets, propsOf);
51
+ if (defs.every(perVariant => perVariant.length === 0)) {
52
+ return undefined;
53
+ }
54
+ return splice(code, instance.content.start, targets, defs);
55
+ }
56
+ // The only shape a component can arrive in.
57
+ function importedComponents(body) {
58
+ const found = new Map();
59
+ for (const node of body) {
60
+ if (node.type !== 'ImportDeclaration' || !/\.svelte$/.test(node.source?.value ?? '')) {
61
+ continue;
62
+ }
63
+ for (const specifier of node.specifiers ?? []) {
64
+ if (specifier.type === 'ImportDefaultSpecifier') {
65
+ found.set(specifier.local.name, node.source.value);
66
+ }
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+ /**
72
+ * Numbered within its variant, which is how `_hPropState` is keyed. Undefined
73
+ * means "do not touch this story": a variant a block produces is registered in
74
+ * an order no static pass can predict.
75
+ */
76
+ function collectTargets(fragment, imported) {
77
+ const story = findStory(fragment);
78
+ if (!story || blockWrapsAVariant(story.fragment)) {
79
+ return undefined;
80
+ }
81
+ // At any depth and in document order, matching how `collect/Variant.svelte`
82
+ // registers them. A story with none renders its children as variant zero.
83
+ const variants = [];
84
+ walk(story.fragment, (node) => {
85
+ if (isHst(node, 'Variant')) {
86
+ variants.push(node);
87
+ }
88
+ });
89
+ const scopes = variants.length > 0 ? variants : [story];
90
+ const targets = [];
91
+ scopes.forEach((scope, variant) => {
92
+ let index = 0;
93
+ walk(scope.fragment, (node, ancestors) => {
94
+ // The controls snippet builds the panel, not the preview. A component in
95
+ // a block is skipped on its own.
96
+ if (isControls(node)) {
97
+ return false;
98
+ }
99
+ if (node.type !== 'Component' || insideBlock(ancestors)) {
100
+ return undefined;
101
+ }
102
+ const specifier = imported.get(node.name);
103
+ if (specifier) {
104
+ targets.push({ node, variant, index: index++, specifier });
105
+ }
106
+ return undefined;
107
+ });
108
+ });
109
+ return targets;
110
+ }
111
+ function findStory(fragment) {
112
+ let found;
113
+ walk(fragment, (node) => {
114
+ if (!found && isHst(node, 'Story')) {
115
+ found = node;
116
+ }
117
+ });
118
+ return found;
119
+ }
120
+ function blockWrapsAVariant(fragment) {
121
+ let wrapped = false;
122
+ walk(fragment, (node, ancestors) => {
123
+ if (isHst(node, 'Variant') && insideBlock(ancestors)) {
124
+ wrapped = true;
125
+ }
126
+ });
127
+ return wrapped;
128
+ }
129
+ function insideBlock(ancestors) {
130
+ return ancestors.some(ancestor => ancestor.type.endsWith('Block') && ancestor.type !== 'SnippetBlock');
131
+ }
132
+ function isControls(node) {
133
+ if (node.type === 'SnippetBlock') {
134
+ return node.expression?.name === 'controls';
135
+ }
136
+ return (node.attributes ?? []).some((attribute) => attribute.name === 'slot' && attribute.value?.[0]?.data === 'controls');
137
+ }
138
+ function isHst(node, member) {
139
+ return node.type === 'Component' && (member ? node.name === `${HST}.${member}` : node.name?.startsWith(`${HST}.`));
140
+ }
141
+ // `then`/`catch`/`pending` and `fallback` are branches a variant can hide in.
142
+ const CHILD_KEYS = ['fragment', 'nodes', 'children', 'body', 'consequent', 'alternate', 'pending', 'then', 'catch', 'fallback'];
143
+ function walk(node, visit, ancestors = []) {
144
+ if (!node || typeof node !== 'object') {
145
+ return;
146
+ }
147
+ if (Array.isArray(node)) {
148
+ for (const child of node)
149
+ walk(child, visit, ancestors);
150
+ return;
151
+ }
152
+ if (typeof node.type === 'string') {
153
+ if (visit(node, ancestors) === false) {
154
+ return;
155
+ }
156
+ ancestors = [...ancestors, node];
157
+ }
158
+ for (const key of CHILD_KEYS) {
159
+ if (node[key])
160
+ walk(node[key], visit, ancestors);
161
+ }
162
+ }
163
+ async function describe(targets, propsOf) {
164
+ const sources = new Map();
165
+ for (const target of targets) {
166
+ if (!sources.has(target.specifier)) {
167
+ sources.set(target.specifier, propsOf(target.specifier));
168
+ }
169
+ }
170
+ const size = targets.reduce((max, target) => Math.max(max, target.variant + 1), 0);
171
+ const defs = Array.from({ length: size }, () => []);
172
+ for (const target of targets) {
173
+ const props = await sources.get(target.specifier);
174
+ if (props?.length) {
175
+ defs[target.variant].push({ name: target.node.name, index: target.index, props });
176
+ }
177
+ }
178
+ return defs;
179
+ }
180
+ /**
181
+ * Back to front, so an earlier insertion cannot move a later offset. The spread
182
+ * goes after the last attribute: last wins, so a prop the story binds keeps
183
+ * winning until the reader touches that control.
184
+ */
185
+ function splice(code, scriptStart, targets, defs) {
186
+ const driven = new Set(defs.flatMap((perVariant, variant) => perVariant.map(def => `${variant}:${def.index}`)));
187
+ const edits = targets
188
+ .filter(target => driven.has(`${target.variant}:${target.index}`))
189
+ .map(target => ({
190
+ at: attributesEnd(code, target.node),
191
+ text: ` {...$${HOLDER}[${target.variant}][${target.index}]}`,
192
+ }));
193
+ // One line, at the end of the `<script>` line, so nothing the story wrote moves.
194
+ edits.push({ at: scriptStart, text: preamble(defs) });
195
+ let result = code;
196
+ for (const edit of edits.sort((a, b) => b.at - a.at)) {
197
+ result = result.slice(0, edit.at) + edit.text + result.slice(edit.at);
198
+ }
199
+ return result;
200
+ }
201
+ // Serialised once and referenced, or a hundred-variant story carries a hundred
202
+ // copies of the same prop table.
203
+ function preamble(defs) {
204
+ const table = [];
205
+ const entries = defs.map(perVariant => perVariant.map((def) => {
206
+ const serialised = literal({ name: def.name, props: def.props });
207
+ let slot = table.indexOf(serialised);
208
+ if (slot < 0) {
209
+ slot = table.push(serialised) - 1;
210
+ }
211
+ return `{...${TABLE}[${slot}],index:${def.index}}`;
212
+ }));
213
+ return `import { autoProps as __pvtCreateAutoProps } from '${RUNTIME}';`
214
+ + `const ${TABLE} = [${table.join(',')}];`
215
+ + `const ${HOLDER} = __pvtCreateAutoProps([${entries.map(perVariant => `[${perVariant.join(',')}]`).join(',')}]);`;
216
+ }
217
+ // `</script>` in a default would close the block this lands in.
218
+ function literal(value) {
219
+ return JSON.stringify(value)
220
+ .replace(/</g, '\\u003C')
221
+ .replace(/\u2028/g, '\\u2028')
222
+ .replace(/\u2029/g, '\\u2029');
223
+ }
224
+ function attributesEnd(code, node) {
225
+ const attributes = node.attributes ?? [];
226
+ if (attributes.length > 0) {
227
+ return Math.max(...attributes.map((attribute) => attribute.end));
228
+ }
229
+ return code.indexOf(node.name, node.start) + node.name.length;
230
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Whether to warn that a variant has controls but no `initState`.
3
+ *
4
+ * The warning is real — a controls snippet with no seeded state renders controls
5
+ * whose edits cannot reach the story, because the story is mounted separately per
6
+ * slot. But it fired on stories where the state *is* seeded, from `initState` on
7
+ * the `Story` rather than the `Variant`, and told their authors that working
8
+ * controls were broken (#462).
9
+ *
10
+ * `seeded` is the signal it was missing: `RenderStory` sets `__pvtStateSeeded`
11
+ * when it applies a story-level `initState`, on the same object this already
12
+ * holds. Only the variant-level check was wrong; the story-level one is correct.
13
+ */
14
+ export declare function shouldWarnAboutMissingInitState(options: {
15
+ hasInitState: boolean;
16
+ seeded: boolean;
17
+ slotName: string;
18
+ hasControlsSlot: boolean;
19
+ alreadyWarned: boolean;
20
+ }): boolean;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Whether to warn that a variant has controls but no `initState`.
3
+ *
4
+ * The warning is real — a controls snippet with no seeded state renders controls
5
+ * whose edits cannot reach the story, because the story is mounted separately per
6
+ * slot. But it fired on stories where the state *is* seeded, from `initState` on
7
+ * the `Story` rather than the `Variant`, and told their authors that working
8
+ * controls were broken (#462).
9
+ *
10
+ * `seeded` is the signal it was missing: `RenderStory` sets `__pvtStateSeeded`
11
+ * when it applies a story-level `initState`, on the same object this already
12
+ * holds. Only the variant-level check was wrong; the story-level one is correct.
13
+ */
14
+ export function shouldWarnAboutMissingInitState(options) {
15
+ const { hasInitState, seeded, slotName, hasControlsSlot, alreadyWarned } = options;
16
+ if (hasInitState || seeded || alreadyWarned) {
17
+ return false;
18
+ }
19
+ // Controls mount only: the two mounts do not share a realm for iframe layouts,
20
+ // so this is what keeps it to one warning.
21
+ return slotName === 'controls' && hasControlsSlot;
22
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@poveste/plugin-svelte",
3
3
  "type": "module",
4
- "version": "0.11.0",
4
+ "version": "0.12.1",
5
5
  "description": "Poveste plugin for Svelte 5 and SvelteKit support",
6
6
  "author": {
7
- "name": "Sorin Gitlan"
7
+ "name": "Sorin Gitlan",
8
+ "url": "https://x.com/50rayn"
8
9
  },
9
10
  "license": "MIT",
10
11
  "homepage": "https://poveste.dev",
@@ -41,6 +42,10 @@
41
42
  "types": "./dist/collect/index.d.ts",
42
43
  "default": "./dist/collect/index.js"
43
44
  },
45
+ "./auto-props": {
46
+ "types": "./dist/client/auto-props.d.ts",
47
+ "default": "./dist/client/auto-props.js"
48
+ },
44
49
  ".": "./dist/index.node.js",
45
50
  "./*": "./*"
46
51
  },
@@ -49,6 +54,7 @@
49
54
  "types": "./dist/index.node.d.ts",
50
55
  "files": [
51
56
  "assets",
57
+ "auto-props.d.ts",
52
58
  "client.d.ts",
53
59
  "collect.d.ts",
54
60
  "dist"
@@ -59,8 +65,8 @@
59
65
  "peerDependencies": {
60
66
  "@sveltejs/kit": "^2.53.0",
61
67
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
62
- "svelte": "^5.46.4",
63
- "poveste": "^0.11.0"
68
+ "poveste": "^0.12.1",
69
+ "svelte": "^5.46.4"
64
70
  },
65
71
  "peerDependenciesMeta": {
66
72
  "@sveltejs/kit": {
@@ -68,13 +74,13 @@
68
74
  }
69
75
  },
70
76
  "dependencies": {
77
+ "@poveste/controls": "^0.12.1",
78
+ "@poveste/shared": "^0.12.1",
79
+ "@poveste/vendors": "^0.12.1",
71
80
  "change-case": "^5.4.4",
72
81
  "globby": "^14.0.2",
73
82
  "launch-editor": "^2.9.1",
74
- "pathe": "^1.1.2",
75
- "@poveste/controls": "^0.11.0",
76
- "@poveste/shared": "^0.11.0",
77
- "@poveste/vendors": "^0.11.0"
83
+ "pathe": "^1.1.2"
78
84
  },
79
85
  "devDependencies": {
80
86
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
@@ -82,12 +88,12 @@
82
88
  "concurrently": "^9.1.0",
83
89
  "fs-extra": "^11.2.0",
84
90
  "globby": "^14.0.2",
91
+ "poveste": "0.12.1",
85
92
  "svelte": "5.55.0",
86
93
  "svelte-preprocess": "^6.0.3",
87
94
  "typescript": "5.6.3",
88
95
  "vite": "^8.0.0",
89
- "vitest": "^4.1.10",
90
- "poveste": "0.11.0"
96
+ "vitest": "^4.1.10"
91
97
  },
92
98
  "scripts": {
93
99
  "build": "rimraf dist && vite build && tsc -d -P tsconfig.build.json && pnpm run build:types",