|
| 1 | +import fs from "fs/promises"; |
| 2 | +import path from "path"; |
| 3 | +import type CEM from "@ui5/webcomponents-tools/lib/cem/types.d.ts"; |
| 4 | + |
| 5 | +const STORIES_ROOT_FOLDER_NAME = '../_stories'; |
| 6 | + |
| 7 | +const isCustomElementDeclaration = (object: any): object is CEM.CustomElementDeclaration => { |
| 8 | + return "customElement" in object && object.customElement; |
| 9 | +}; |
| 10 | + |
| 11 | +type ControlType = "text" | "select" | "multi-select" | boolean; |
| 12 | + |
| 13 | +type ArgsTypes = { |
| 14 | + [key: string]: { |
| 15 | + control?: ControlType | { type: ControlType; /* See below for more */ }; |
| 16 | + description?: string; |
| 17 | + mapping?: { [key: string]: { [option: string]: any } }; |
| 18 | + name?: string; |
| 19 | + options?: string[]; |
| 20 | + table?: { |
| 21 | + category?: string; |
| 22 | + defaultValue?: { summary: string; detail?: string }; |
| 23 | + subcategory?: string; |
| 24 | + type?: { summary?: string; detail?: string }; |
| 25 | + }, |
| 26 | + UI5CustomData?: { |
| 27 | + parameters?: Array<CEM.Parameter>, |
| 28 | + returnValue?: { |
| 29 | + description?: string |
| 30 | + summary?: string |
| 31 | + type?: CEM.Type |
| 32 | + } |
| 33 | + } |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +type APIData = { |
| 38 | + info: { |
| 39 | + package: string; |
| 40 | + since: string | undefined; |
| 41 | + }; |
| 42 | + slotNames: Array<string>; |
| 43 | + storyArgsTypes: string; |
| 44 | +} |
| 45 | + |
| 46 | +// run the script to generate the argTypes for the stories available in the _stories folder |
| 47 | +const main = async () => { |
| 48 | + const api: CEM.MySchema = JSON.parse((await fs.readFile(`./.storybook/custom-elements.json`)).toString()); |
| 49 | + |
| 50 | + // read all directories inside _stories folder and create a list of components |
| 51 | + const packages = await fs.readdir(path.join(__dirname, STORIES_ROOT_FOLDER_NAME)); |
| 52 | + for (const currPackage of packages) { |
| 53 | + // packages [main, fiori] |
| 54 | + |
| 55 | + const packagePath = path.join(__dirname, STORIES_ROOT_FOLDER_NAME, currPackage); |
| 56 | + const packageStats = await fs.stat(packagePath); |
| 57 | + if (packageStats.isDirectory()) { |
| 58 | + const componentsInPackage = await fs.readdir(packagePath); |
| 59 | + for (const component of componentsInPackage) { |
| 60 | + // components [Button, Card, ...] |
| 61 | + const componentPath = path.join(packagePath, component); |
| 62 | + const componentStats = await fs.stat(componentPath); |
| 63 | + if (componentStats.isDirectory()) { |
| 64 | + generateStoryDoc(componentPath, component, api, currPackage); |
| 65 | + } |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + async function generateStoryDoc(componentPath: string, component: string, api: CEM.MySchema, componentPackage: string) { |
| 71 | + console.log(`Generating argTypes for story ${component}`); |
| 72 | + const apiData = getAPIData(api, component, componentPackage); |
| 73 | + |
| 74 | + if (!apiData) { |
| 75 | + return; |
| 76 | + } |
| 77 | + |
| 78 | + const { storyArgsTypes, slotNames, info } = apiData; |
| 79 | + |
| 80 | + await fs.writeFile(componentPath + '/argTypes.ts', `export default ${storyArgsTypes}; |
| 81 | +export const componentInfo = ${JSON.stringify(info, null, 4)}; |
| 82 | +export type StoryArgsSlots = { |
| 83 | + ${slotNames.map(slotName => `${slotName}: string;`).join('\n ')} |
| 84 | +}`); |
| 85 | + }; |
| 86 | + |
| 87 | + function getAPIData(api: CEM.MySchema, module: string, componentPackage: string): APIData | undefined { |
| 88 | + const moduleAPI = api.modules?.find(currModule => currModule.declarations?.find(s => s._ui5reference?.name === module && s._ui5reference?.package === `@ui5/webcomponents${componentPackage !== 'main' ? `-${componentPackage}` : ''}`)); |
| 89 | + const declaration = moduleAPI?.declarations?.find(s => s._ui5reference?.name === module && s._ui5reference?.package === `@ui5/webcomponents${componentPackage !== 'main' ? `-${componentPackage}` : ''}`); |
| 90 | + |
| 91 | + if (!declaration) { |
| 92 | + return; |
| 93 | + } |
| 94 | + |
| 95 | + const data = getArgsTypes(api, declaration as CEM.CustomElementDeclaration, componentPackage); |
| 96 | + |
| 97 | + return { |
| 98 | + info: { |
| 99 | + package: `@ui5/webcomponents${componentPackage !== 'main' ? `-${componentPackage}` : ''}`, |
| 100 | + since: declaration?._ui5since |
| 101 | + }, |
| 102 | + slotNames: data.slotNames, |
| 103 | + storyArgsTypes: JSON.stringify(data.args, null, "\t") |
| 104 | + }; |
| 105 | + } |
| 106 | + |
| 107 | + function getArgsTypes(api: CEM.MySchema, moduleAPI: CEM.CustomElementDeclaration | CEM.ClassDeclaration, componentPackage: string): { args: any, slotNames: Array<string> } { |
| 108 | + let args: ArgsTypes = {}; |
| 109 | + let slotNames: Array<string> = []; |
| 110 | + |
| 111 | + moduleAPI.members |
| 112 | + ?.filter((member): member is CEM.ClassField => "kind" in member && member.kind === "field") |
| 113 | + .forEach(prop => { |
| 114 | + let typeEnum: CEM.EnumDeclaration | undefined; |
| 115 | + |
| 116 | + if (prop.type?.references?.length) { |
| 117 | + for (let currModule of api.modules) { |
| 118 | + if (!currModule.declarations) { |
| 119 | + continue; |
| 120 | + } |
| 121 | + |
| 122 | + for (let s of currModule.declarations) { |
| 123 | + if (s?._ui5reference?.name === prop.type?.references[0].name && s?._ui5reference?.package === prop.type?.references[0].package && s.kind === "enum") { |
| 124 | + typeEnum = s; |
| 125 | + break; |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + if (prop.readonly) { |
| 132 | + args[prop.name] = { |
| 133 | + control: { |
| 134 | + type: false |
| 135 | + }, |
| 136 | + }; |
| 137 | + } else if (typeEnum && Array.isArray(typeEnum.members)) { |
| 138 | + args[prop.name] = { |
| 139 | + control: "select", |
| 140 | + options: typeEnum.members.map(a => a.name), |
| 141 | + }; |
| 142 | + } |
| 143 | + }); |
| 144 | + |
| 145 | + if (isCustomElementDeclaration(moduleAPI)) { |
| 146 | + moduleAPI.slots?.forEach(prop => { |
| 147 | + args[prop.name] = { |
| 148 | + control: { |
| 149 | + type: "text" |
| 150 | + } |
| 151 | + }; |
| 152 | + slotNames.push(prop.name); |
| 153 | + }); |
| 154 | + } |
| 155 | + |
| 156 | + // methods parsing because Storybook does not include them in the args by default from the custom-elements.json |
| 157 | + // only changing the category to Methods so they are not displayed in the Properties tab |
| 158 | + moduleAPI.members |
| 159 | + ?.filter((member): member is CEM.ClassMethod => "kind" in member && member.kind === "method") |
| 160 | + .forEach((prop) => { |
| 161 | + args[prop.name] = { |
| 162 | + description: prop.description, |
| 163 | + table: { |
| 164 | + category: "methods", |
| 165 | + }, |
| 166 | + }; |
| 167 | + |
| 168 | + // methods can have custom descriptions with parameters and return value |
| 169 | + if (prop.parameters || prop.return) { |
| 170 | + args[prop.name].UI5CustomData = { |
| 171 | + parameters: prop.parameters, |
| 172 | + returnValue: prop.return, |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + (prop as unknown as CEM.ClassField).kind = "field"; |
| 177 | + }); |
| 178 | + |
| 179 | + // events also have custom descriptions with parameters of their detail objec |
| 180 | + if (isCustomElementDeclaration(moduleAPI)) { |
| 181 | + moduleAPI.events?.forEach((prop) => { |
| 182 | + if (prop.privacy === "public" && prop.params?.length) { |
| 183 | + args[prop.name] = { |
| 184 | + description: prop.description, |
| 185 | + table: { |
| 186 | + category: "events", |
| 187 | + }, |
| 188 | + UI5CustomData: { |
| 189 | + parameters: prop.params, |
| 190 | + }, |
| 191 | + }; |
| 192 | + } |
| 193 | + }); |
| 194 | + } |
| 195 | + |
| 196 | + const packages = ["@ui5/webcomponents", "@ui5/webcomponents-fiori"] |
| 197 | + |
| 198 | + // recursively merging the args from the parent/parents |
| 199 | + const moduleAPIBeingExtended = moduleAPI.superclass && api.modules |
| 200 | + ?.find(currModule => currModule.declarations |
| 201 | + ?.find(s => s?._ui5reference?.name === moduleAPI.superclass?.name && s?._ui5reference?.package === moduleAPI.superclass?.package)) |
| 202 | + ?.declarations |
| 203 | + ?.find(s => s?._ui5reference?.name === moduleAPI.superclass?.name && s?._ui5reference?.package === moduleAPI.superclass?.package) as CEM.ClassDeclaration; |
| 204 | + |
| 205 | + const referencePackage = moduleAPIBeingExtended?._ui5reference?.package |
| 206 | + |
| 207 | + if (moduleAPIBeingExtended && referencePackage && packages.includes(referencePackage)) { |
| 208 | + const { args: nextArgs, slotNames: nextSlotNames } = getArgsTypes(api, moduleAPIBeingExtended, referencePackage === "@ui5/webcomponents" ? "main" : "fiori"); |
| 209 | + args = { ...args, ...nextArgs }; |
| 210 | + slotNames = [...slotNames, ...nextSlotNames].filter((v, i, a) => a.indexOf(v) === i); |
| 211 | + } |
| 212 | + |
| 213 | + return { |
| 214 | + args, |
| 215 | + slotNames |
| 216 | + }; |
| 217 | + } |
| 218 | +}; |
| 219 | + |
| 220 | +main(); |
0 commit comments