Skip to content

Commit

Permalink
svelte docgen: enrich stories
Browse files Browse the repository at this point in the history
  • Loading branch information
ciscorn committed Jul 12, 2024
1 parent a4f4a6a commit 95d5665
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 63 deletions.
2 changes: 2 additions & 0 deletions code/frameworks/svelte-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@babel/generator": "^7.24.8",
"@babel/parser": "^7.24.8",
"@babel/traverse": "^7.24.7",
"@storybook/builder-vite": "workspace:*",
"@storybook/svelte": "workspace:*",
Expand Down
112 changes: 62 additions & 50 deletions code/frameworks/svelte-vite/src/plugins/generateDocgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ export type PropInfo = {
runes?: boolean;
};

export type EventInfo = {
name: string;
};

type BaseType = {
/** Permits undefined or not */
optional?: boolean;
Expand All @@ -32,41 +28,33 @@ type BaseType = {
type ScalarType = BaseType & {
type: 'number' | 'string' | 'boolean' | 'symbol' | 'any' | 'null';
};

type FunctionType = BaseType & {
type: 'function';
text: string;
};

type LiteralType = BaseType & {
type: 'literal';
value: string | number | boolean;
text: string;
};

type ArrayType = BaseType & {
type: 'array';
};

type ObjectType = BaseType & {
type: 'object';
};

type UnionType = BaseType & {
type: 'union';
types: Type[];
};

type IntersectionType = BaseType & {
type: 'intersection';
types: Type[];
};

type ReferenceType = BaseType & {
type: 'reference';
text: string;
};

type OtherType = BaseType & {
type: 'other';
text: string;
Expand All @@ -84,7 +72,7 @@ export type Type =
| IntersectionType;

/**
* Try to infer a type from a initializer expression (for when there is no type annotation)
* Try to infer a type from an initializer expression (for when there is no type annotation)
*/
function inferTypeFromInitializer(expr: Expression): Type | undefined {
switch (expr.type) {
Expand Down Expand Up @@ -171,11 +159,11 @@ function parseType(type: TSType): Type | undefined {
}
} else if (type.type == 'TSIntersectionType') {
// e.g. `A & B`
const types: Type[] = type.types
const types = type.types
.map((t) => {
return parseType(t);
})
.filter((t) => t !== undefined);
.filter((t) => t !== undefined) as Type[];
return { type: 'intersection', types };
}
return undefined;
Expand All @@ -197,8 +185,7 @@ function tryParseJSDocType(text: string): Type | undefined {
for (const decl of stmt.declarations) {
if (decl.id.type == 'Identifier') {
if (decl.id.typeAnnotation?.type === 'TSTypeAnnotation') {
const a = parseType(decl.id.typeAnnotation.typeAnnotation);
return a;
return parseType(decl.id.typeAnnotation.typeAnnotation);
}
}
}
Expand All @@ -207,7 +194,7 @@ function tryParseJSDocType(text: string): Type | undefined {
}

/**
* Extract JSDoc comments
* Parse JSDoc comments
*/
function parseComments(leadingComments?: Comment[] | null) {
if (!leadingComments) {
Expand Down Expand Up @@ -267,46 +254,37 @@ export function generateDocgen(fileContent: string): Docgen {
}

const propMap: Map<string, PropInfo> = new Map();
// const events: EventInfo[] = [];
let propTypeName = '$$ComponentProps';

traverse(ast, {
FunctionDeclaration: (funcPath) => {
if (funcPath.node.id && funcPath.node.id.name !== 'render') {
return;
}
funcPath.traverse({
TSTypeAliasDeclaration(path) {
if (
path.node.id.name !== '$$ComponentProps' ||
path.node.typeAnnotation.type !== 'TSTypeLiteral'
) {
ReturnStatement: (path) => {
// For runes mode: Get the name of props type alias from `return { props: {} as MyProps, ... }`
if (path.parent !== funcPath.node.body) {
return;
}
const members = path.node.typeAnnotation.members;
members.forEach((member) => {
if (member.type === 'TSPropertySignature' && member.key.type === 'Identifier') {
const name = member.key.name;

const type =
member.typeAnnotation && member.typeAnnotation.type === 'TSTypeAnnotation'
? parseType(member.typeAnnotation.typeAnnotation)
: undefined;

if (type && member.optional) {
type.optional = true;
const argument = path.node.argument;
if (argument?.type === 'ObjectExpression') {
argument.properties.forEach((property) => {
if (property.type === 'ObjectProperty') {
if (property.key.type === 'Identifier' && property.key.name === 'props') {
if (property.value.type == 'TSAsExpression') {
const typeAnnotation = property.value.typeAnnotation;
if (
typeAnnotation?.type === 'TSTypeReference' &&
typeAnnotation.typeName.type === 'Identifier'
) {
propTypeName = typeAnnotation.typeName.name;
}
}
}
}

const { description } = parseComments(member.leadingComments);

propMap.set(name, {
...propMap.get(name),
name,
type: type,
description,
runes: true,
});
}
});
});
}
},
VariableDeclaration: (path) => {
if (path.node.kind !== 'let' || path.parent !== funcPath.node.body) {
Expand All @@ -319,7 +297,7 @@ export function generateDocgen(fileContent: string): Docgen {
declaration.id.typeAnnotation &&
declaration.id.typeAnnotation.type === 'TSTypeAnnotation'
) {
// Get default values from Svelte 5's `let { ... } = $props();`
// For runes mode: Collect default values from `let { ... } = $props();`

const typeAnnotation = declaration.id.typeAnnotation.typeAnnotation;
if (
Expand Down Expand Up @@ -350,7 +328,7 @@ export function generateDocgen(fileContent: string): Docgen {
}
});
} else if (declaration.id.type === 'Identifier') {
// Get props from Svelte 4's `export let a = ...`
// For legacy mode: Collect props info from `export let a = ...`

const name = declaration.id.name;
if (tsx.exportedNames.has(name)) {
Expand Down Expand Up @@ -393,6 +371,40 @@ export function generateDocgen(fileContent: string): Docgen {
},
});

// For runes mode: Try to find and parse the props type alias.
traverse(ast, {
TSTypeAliasDeclaration(path) {
if (path.node.id.name !== propTypeName || path.node.typeAnnotation.type !== 'TSTypeLiteral') {
return;
}
const members = path.node.typeAnnotation.members;
members.forEach((member) => {
if (member.type === 'TSPropertySignature' && member.key.type === 'Identifier') {
const name = member.key.name;

const type =
member.typeAnnotation && member.typeAnnotation.type === 'TSTypeAnnotation'
? parseType(member.typeAnnotation.typeAnnotation)
: undefined;

if (type && member.optional) {
type.optional = true;
}

const { description } = parseComments(member.leadingComments);

propMap.set(name, {
...propMap.get(name),
name,
type: type,
description,
runes: true,
});
}
});
},
});

return {
props: Array.from(propMap.values()),
};
Expand Down
16 changes: 10 additions & 6 deletions code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import MagicString from 'magic-string';
import path from 'path';
import fs from 'fs';
import svelteDoc from 'sveltedoc-parser';
import type { SvelteComponentDoc, SvelteParserOptions, JSDocType } from 'sveltedoc-parser';
import type {
SvelteComponentDoc,
SvelteDataItem,
SvelteParserOptions,
JSDocType,
} from 'sveltedoc-parser';
import { logger } from 'storybook/internal/node-logger';
import { preprocess } from 'svelte/compiler';
import { replace, typescript } from 'svelte-preprocess';
Expand Down Expand Up @@ -98,8 +103,8 @@ function formatToSvelteDocParserType(type: Type): JSDocType {
}
}

function emulateSvelteDocParserDataItems(docgen: Docgen) {
const data = docgen.props.map((p) => {
function transformToSvelteDocParserDataItems(docgen: Docgen): SvelteDataItem[] {
return docgen.props.map((p) => {
const required = p.runes && p.defaultValue === undefined && p.type && !p.type.optional;
return {
name: p.name,
Expand All @@ -114,9 +119,8 @@ function emulateSvelteDocParserDataItems(docgen: Docgen) {
originalName: undefined,
localName: undefined,
defaultValue: p.defaultValue,
};
} satisfies SvelteDataItem;
});
return data;
}

export async function svelteDocgen(svelteOptions: Record<string, any> = {}): Promise<PluginOption> {
Expand All @@ -140,7 +144,7 @@ export async function svelteDocgen(svelteOptions: Record<string, any> = {}): Pro
// Get props information
const docgen = generateDocgen(rawSource);
const hasRuneProps = docgen.props.some((p) => p.runes);
const data = emulateSvelteDocParserDataItems(docgen);
const data = transformToSvelteDocParserDataItems(docgen);

let componentDoc: SvelteComponentDoc & { keywords?: string[] } = {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// @ts-ignore
const Button = globalThis.Components?.Button;
import { createEventDispatcher } from 'svelte';
/**
* Rounds the button
*/
Expand All @@ -23,14 +25,31 @@
*/
export let text: string = 'You clicked';
const dispatch = createEventDispatcher();
function handleClick(_event: MouseEvent) {
count += 1;
}
function onMouseHover(event) {
dispatch('mousehover', event);
}
</script>

<h1>Button TypeScript</h1>

<Button {primary} on:click on:click={handleClick} label="{text}: {count}" />
<Button
{primary}
on:click
on:click={handleClick}
on:mousehover={onMouseHover}
label="{text}: {count}"
/>

<!-- Default slot -->
<slot foo={count} />
<!-- Named slot -->
<slot name="namedSlot1" bar={text} />

<p>A little text to show this is a view.</p>
<p>If we need to test components in a Svelte environment, for instance to test slot behaviour,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// @ts-ignore
const Button = globalThis.Components?.Button;
import { createEventDispatcher } from 'svelte';
/**
* Rounds the button
*/
Expand All @@ -23,14 +25,37 @@
*/
export let text: string = 'You clicked';
/**
* How large should the button be?
*/
export let size: 'large' | 'medium' | 'small' = 'medium';
const dispatch = createEventDispatcher();
function handleClick(_event: MouseEvent) {
count += 1;
}
function onMouseHover(event) {
dispatch('mousehover', event);
}
</script>

<h1>Button TypeScript</h1>

<Button {primary} on:click on:click={handleClick} label="{text}: {count}" />
<Button
{primary}
{size}
on:click
on:click={handleClick}
on:mousehover={onMouseHover}
label="{text}: {count}"
/>

<!-- Default slot -->
<slot foo={count} />
<!-- Named slot -->
<slot name="namedSlot1" bar={text} />

<p>A little text to show this is a view.</p>
<p>If we need to test components in a Svelte environment, for instance to test slot behaviour,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
children,
label,
}: {
/**
* Is this the principal call to action on the page?
*/
Expand Down
Loading

0 comments on commit 95d5665

Please sign in to comment.