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 +15 -0
- package/dist/explorer/views/ComponentView.svelte +5 -2
- package/dist/server/snippet-compiler.d.ts +1 -1
- package/dist/server/snippet-compiler.js +15 -6
- package/dist/server/snippet-compiler.test.d.ts +1 -0
- package/dist/server/snippet-compiler.test.js +23 -0
- package/dist/vite.js +2 -1
- package/package.json +1 -1
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
|
-
|
|
110
|
-
|
|
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
|
|
39
|
-
* its exported methods and state.
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|