From 95d5665dbaf91a541d56e0b733ba76734af558cd Mon Sep 17 00:00:00 2001 From: Taku Fukada Date: Fri, 12 Jul 2024 16:53:10 +0900 Subject: [PATCH] svelte docgen: enrich stories --- code/frameworks/svelte-vite/package.json | 2 + .../svelte-vite/src/plugins/generateDocgen.ts | 112 ++++++++++-------- .../svelte-vite/src/plugins/svelte-docgen.ts | 16 ++- .../ButtonTypeScript.svelte | 21 +++- .../ButtonTypeScript.svelte | 27 ++++- ...s.svelte => ButtonTypeScriptRunes1.svelte} | 1 + .../ButtonTypeScriptRunes2.svelte | 55 +++++++++ ...s.stories.js => ts-runes1-docs.stories.js} | 4 +- .../ts-runes2-docs.stories.js | 13 ++ .../ButtonTypeScript.svelte | 21 +++- .../template/stories/jsdoc-docs.stories.js | 11 ++ .../template/stories/views/ButtonJSDoc.svelte | 64 ++++++++++ .../stories/views/ButtonJavaScript.svelte | 29 ++++- 13 files changed, 313 insertions(+), 63 deletions(-) rename code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/{ButtonTypeScriptRunes.svelte => ButtonTypeScriptRunes1.svelte} (99%) create mode 100644 code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ButtonTypeScriptRunes2.svelte rename code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/{ts-runes-docs.stories.js => ts-runes1-docs.stories.js} (57%) create mode 100644 code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes2-docs.stories.js create mode 100644 code/renderers/svelte/template/stories/jsdoc-docs.stories.js create mode 100644 code/renderers/svelte/template/stories/views/ButtonJSDoc.svelte diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index 601dd8346a85..6c806f3b2f78 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -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:*", diff --git a/code/frameworks/svelte-vite/src/plugins/generateDocgen.ts b/code/frameworks/svelte-vite/src/plugins/generateDocgen.ts index 7fd191499ab5..7a1ab4052905 100644 --- a/code/frameworks/svelte-vite/src/plugins/generateDocgen.ts +++ b/code/frameworks/svelte-vite/src/plugins/generateDocgen.ts @@ -20,10 +20,6 @@ export type PropInfo = { runes?: boolean; }; -export type EventInfo = { - name: string; -}; - type BaseType = { /** Permits undefined or not */ optional?: boolean; @@ -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; @@ -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) { @@ -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; @@ -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); } } } @@ -207,7 +194,7 @@ function tryParseJSDocType(text: string): Type | undefined { } /** - * Extract JSDoc comments + * Parse JSDoc comments */ function parseComments(leadingComments?: Comment[] | null) { if (!leadingComments) { @@ -267,7 +254,7 @@ export function generateDocgen(fileContent: string): Docgen { } const propMap: Map = new Map(); - // const events: EventInfo[] = []; + let propTypeName = '$$ComponentProps'; traverse(ast, { FunctionDeclaration: (funcPath) => { @@ -275,38 +262,29 @@ export function generateDocgen(fileContent: string): Docgen { 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) { @@ -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 ( @@ -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)) { @@ -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()), }; diff --git a/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts b/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts index 2d5b9d16bf95..8733ed697fe8 100644 --- a/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts +++ b/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts @@ -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'; @@ -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, @@ -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 = {}): Promise { @@ -140,7 +144,7 @@ export async function svelteDocgen(svelteOptions: Record = {}): 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[] } = {}; diff --git a/code/frameworks/svelte-vite/template/stories_svelte-vite-default-ts/ButtonTypeScript.svelte b/code/frameworks/svelte-vite/template/stories_svelte-vite-default-ts/ButtonTypeScript.svelte index cd00f38a3d57..ecd4ab2c79f5 100644 --- a/code/frameworks/svelte-vite/template/stories_svelte-vite-default-ts/ButtonTypeScript.svelte +++ b/code/frameworks/svelte-vite/template/stories_svelte-vite-default-ts/ButtonTypeScript.svelte @@ -7,6 +7,8 @@ // @ts-ignore const Button = globalThis.Components?.Button; + import { createEventDispatcher } from 'svelte'; + /** * Rounds the button */ @@ -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); + }

Button TypeScript

- diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes-docs.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes1-docs.stories.js similarity index 57% rename from code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes-docs.stories.js rename to code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes1-docs.stories.js index daa8ccc4db8f..93ee9838e2c4 100644 --- a/code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes-docs.stories.js +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes1-docs.stories.js @@ -1,7 +1,7 @@ -import ButtonTypescriptRunes from './ButtonTypeScriptRunes.svelte'; +import ButtonTypescriptRunes from './ButtonTypeScriptRunes1.svelte'; export default { - title: 'stories/renderers/svelte/ts-runes-docs', + title: 'stories/renderers/svelte/ts-runes1-docs', component: ButtonTypescriptRunes, args: { primary: true, diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes2-docs.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes2-docs.stories.js new file mode 100644 index 000000000000..86787ea459a3 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-prerelease-ts/ts-runes2-docs.stories.js @@ -0,0 +1,13 @@ +import ButtonTypescriptRunes from './ButtonTypeScriptRunes2.svelte'; + +export default { + title: 'stories/renderers/svelte/ts-runes2-docs', + component: ButtonTypescriptRunes, + args: { + primary: true, + label: 'Button', + }, + tags: ['autodocs'], +}; + +export const Primary = {}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/ButtonTypeScript.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/ButtonTypeScript.svelte index cd00f38a3d57..ecd4ab2c79f5 100644 --- a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/ButtonTypeScript.svelte +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/ButtonTypeScript.svelte @@ -7,6 +7,8 @@ // @ts-ignore const Button = globalThis.Components?.Button; + import { createEventDispatcher } from 'svelte'; + /** * Rounds the button */ @@ -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); + }

Button TypeScript

-