sdocs 0.0.39 → 0.0.40

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/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.40] - 2026-07-04
11
+
12
+ ### Fixed
13
+
14
+ - **Wrapped previews bind to the documented component.** When a preview
15
+ wraps the documented component in another one (a `<Tab>` shown inside
16
+ `<Tabs>`), method calls and live state used to attach to the outer
17
+ wrapper — the first capitalized tag in the snippet. The preview ref now
18
+ binds to the doc's own component wherever it appears in the snippet,
19
+ falling back to the first capitalized tag when it's absent.
20
+ - **Usage-code patching matches the component name exactly.** Attribute
21
+ updates from the controls located the root tag with a prefix search, so
22
+ a component like `Tab` could patch a sibling `<Tabs>` tag instead of its
23
+ own. The tag search now requires a whole-name match.
24
+
10
25
  ## [0.0.39] - 2026-07-04
11
26
 
12
27
  ### Changed
@@ -106,8 +106,11 @@
106
106
  if (changes.length === 0) return snippetBody;
107
107
 
108
108
  // Find the opening tag: <ComponentName ...> or <ComponentName ... />
109
- const tagStart = snippetBody.indexOf(`<${name}`);
110
- if (tagStart === -1) return snippetBody;
109
+ // (whole-name match, so <Tab doesn't hit <Tabs)
110
+ const escapedName = name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
111
+ const tagMatch = snippetBody.match(new RegExp(`<${escapedName}(?=[\\s/>])`));
112
+ if (!tagMatch || tagMatch.index === undefined) return snippetBody;
113
+ const tagStart = tagMatch.index;
111
114
 
112
115
  // Find the end of the opening tag, respecting {} expressions
113
116
  let braceDepth = 0;
@@ -11,7 +11,7 @@ export declare function resolveImportsToAbsolute(imports: string[], docFilePath:
11
11
  /** Generate a virtual Svelte iframe wrapper component for a snippet.
12
12
  * Includes $state for reactive prop updates via postMessage, invokes
13
13
  * component methods on request, and broadcasts exported state values. */
14
- export declare function generateIframeComponent(absoluteImports: string[], snippetBody: string, stateNames?: string[]): string;
14
+ export declare function generateIframeComponent(absoluteImports: string[], snippetBody: string, stateNames?: string[], componentName?: string): string;
15
15
  /** The JS that boots a preview page: mount the wrapper + parent-frame messaging. */
16
16
  export declare function generateMountScript(iframeComponentId: string): string;
17
17
  /** Generate the HTML page served inside the iframe (dev / CLI-build inputs) */
@@ -35,12 +35,21 @@ export function resolveImportsToAbsolute(imports, docFilePath) {
35
35
  return imp;
36
36
  });
37
37
  }
38
- /** Add bind:this to the snippet's root component so the wrapper can reach
39
- * its exported methods and state. Skipped when the author already binds. */
40
- function injectRootRef(snippetBody) {
38
+ /** Add bind:this to the documented component in the snippet so the wrapper
39
+ * can reach its exported methods and state. Binds the first occurrence of
40
+ * the doc's component when named (so wrapped children like
41
+ * <Tabs><Tab/></Tabs> documenting Tab bind the right instance), otherwise
42
+ * the first capitalized tag. Skipped when the author already binds. */
43
+ function injectRootRef(snippetBody, componentName) {
41
44
  if (snippetBody.includes('bind:this'))
42
45
  return snippetBody;
43
- const match = snippetBody.match(/<([A-Z][A-Za-z0-9_]*)/);
46
+ let match = null;
47
+ if (componentName) {
48
+ const escaped = componentName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
49
+ match = snippetBody.match(new RegExp(`<${escaped}(?=[\\s/>])`));
50
+ }
51
+ if (!match)
52
+ match = snippetBody.match(/<[A-Z][A-Za-z0-9_]*(?=[\s/>])/);
44
53
  if (!match || match.index === undefined)
45
54
  return snippetBody;
46
55
  const insertAt = match.index + match[0].length;
@@ -49,7 +58,7 @@ function injectRootRef(snippetBody) {
49
58
  /** Generate a virtual Svelte iframe wrapper component for a snippet.
50
59
  * Includes $state for reactive prop updates via postMessage, invokes
51
60
  * component methods on request, and broadcasts exported state values. */
52
- export function generateIframeComponent(absoluteImports, snippetBody, stateNames = []) {
61
+ export function generateIframeComponent(absoluteImports, snippetBody, stateNames = [], componentName) {
53
62
  const importBlock = absoluteImports.length > 0
54
63
  ? absoluteImports.join('\n') + '\n'
55
64
  : '';
@@ -118,7 +127,7 @@ ${stateBroadcast}
118
127
 
119
128
  <div id="sdocs-preview">
120
129
  {#snippet SdocsPreview(args)}
121
- ${injectRootRef(snippetBody)}
130
+ ${injectRootRef(snippetBody, componentName)}
122
131
  {/snippet}
123
132
  {@render SdocsPreview(args)}
124
133
  </div>`;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { generateIframeComponent } from './snippet-compiler.js';
3
+ describe('root ref injection', () => {
4
+ it('binds the documented component, not the wrapper', () => {
5
+ const out = generateIframeComponent([], '<Tabs>\n\t<Tab {...args} />\n</Tabs>', [], 'Tab');
6
+ expect(out).toContain('<Tab bind:this={__sdocsRef} {...args} />');
7
+ expect(out).not.toContain('<Tabs bind:this');
8
+ });
9
+ it('binds the first capitalized tag when no component is named', () => {
10
+ const out = generateIframeComponent([], '<Card><Button /></Card>', []);
11
+ expect(out).toContain('<Card bind:this={__sdocsRef}>');
12
+ });
13
+ it('falls back to the first capitalized tag when the named component is absent', () => {
14
+ const out = generateIframeComponent([], '<Tabs active={0}>text</Tabs>', [], 'Tab');
15
+ expect(out).toContain('<Tabs bind:this={__sdocsRef} active={0}>');
16
+ });
17
+ it('leaves snippets with an author bind:this untouched', () => {
18
+ const body = '<Tab bind:this={mine} />';
19
+ const out = generateIframeComponent([], body, [], 'Tab');
20
+ expect(out).toContain(body);
21
+ expect(out).not.toContain('bind:this={__sdocsRef}');
22
+ });
23
+ });
package/dist/vite.js CHANGED
@@ -217,7 +217,8 @@ export function sdocsPlugin(userConfig) {
217
217
  return null;
218
218
  const absoluteImports = docImportsCache.get(parsed.docFilePath) ?? [];
219
219
  const stateNames = (entry.componentData?.state ?? []).map((s) => s.name);
220
- return generateIframeComponent(absoluteImports, snippet.body, stateNames);
220
+ const componentName = entry.componentPath?.split('/').pop()?.replace('.svelte', '');
221
+ return generateIframeComponent(absoluteImports, snippet.body, stateNames, componentName);
221
222
  }
222
223
  // Virtual mount script for an emitted preview page
223
224
  if (id.startsWith('\0' + MOUNT_PREFIX)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs",
3
- "version": "0.0.39",
3
+ "version": "0.0.40",
4
4
  "description": "A lightweight documentation tool for Svelte 5 components",
5
5
  "type": "module",
6
6
  "license": "MIT",