Skip to content

Commit

Permalink
chore: extract storybook
Browse files Browse the repository at this point in the history
  • Loading branch information
Nayden Naydenov committed Oct 23, 2023
1 parent 0ebecca commit 00e28cb
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 276 deletions.
2 changes: 1 addition & 1 deletion docs/6-contributing/04-writing-samples.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ The above example includes only the `indeterminate`, `checked` properties in the


## Documentation
The documentation for each component is automatically produced using the `custom-elements.json` file. Additionally, there is an `argTypes.ts` file located beside each `.stories.ts` file. It is generated during build time and contains extra properties that enhance the documentation beyond what is available in the `custom-elements.json` file. This file should not be edited directly, as it can only be modified by the `packages/playground/build-scripts-storybook/samples-prepare.ts` script.
The documentation for each component is automatically produced using the `custom-elements.json` file. Additionally, there is an `argTypes.ts` file located beside each `.stories.ts` file. It is generated during build time and contains extra properties that enhance the documentation beyond what is available in the `custom-elements.json` file. This file should not be edited directly, as it can only be modified by the `packages/playground/build-scripts-storybook/samples-prepare.js` script.

### Docs page
Every story has a `docs` page in the storybook's sidebar. Usually, this page is generated automatically by storybook but it can be customized by adding a `docs` property to the story parameters.
Expand Down
2 changes: 1 addition & 1 deletion packages/base/package-scripts.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const scripts = {
generateStyles: `node "${stylesScript}"`,
generateTemplates: `mkdirp src/generated/templates && cross-env UI5_BASE=true UI5_TS=true node "${LIB}/hbs2ui5/index.js" -d test/elements -o src/generated/templates`,
generateAPI: {
default: "nps generateAPI.prepare generateAPI.preprocess generateAPI.jsdoc generateAPI.cleanup generateAPI.generateCEM",
default: "nps generateAPI.prepare generateAPI.preprocess generateAPI.jsdoc generateAPI.cleanup",
generateCEM: `cem analyze --config "${LIB}/cem/custom-elements-manifest.config.mjs"`,
validateCEM: `ajv validate -s ${LIB}/cem/schema.json -d dist/custom-elements.json --allow-union-types --all-errors`,
prepare: `copy-and-watch "dist/**/*.js" jsdoc-dist/`,
Expand Down
6 changes: 0 additions & 6 deletions packages/playground/.storybook/args/enhanceArgTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ export const enhanceArgTypes = <TRenderer extends Renderer>(
) as typeof userArgTypes)
: userArgTypes;

Object.keys(withExtractedTypes)
.filter(key => key.startsWith("_ui5"))
.forEach(argType => {
withExtractedTypes[argType].name = withExtractedTypes[argType].name.replace("_ui5", "");
})

// enhance descriptions
enhanceArgTypesDescriptions(withExtractedTypes);
return withExtractedTypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class EventDescriptionRenderer implements IDescriptionRenderer {
<React.Fragment key={p.name}>
<b><code>{p.name}</code></b>
<ul>
<li><b>type:</b> {p.type.text}</li>
<li><b>type:</b> {p.type}</li>
<li><b>description:</b> {p.description}</li>
</ul>
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class MethodDescriptionRenderer implements IDescriptionRenderer {
<React.Fragment key={p.name}>
<b><code>{p.name}</code></b>
<ul>
<li><b>type:</b> {p.type.text}</li>
<li><b>type:</b> {p.type}</li>
<li><b>description:</b> {p.description}</li>
</ul>
</React.Fragment>
Expand All @@ -31,7 +31,7 @@ export class MethodDescriptionRenderer implements IDescriptionRenderer {
<>
<p><b>Return Value:</b></p>
<ul>
<li><b>type:</b> {returnValue?.type?.text}</li>
<li><b>type:</b> {returnValue?.type}</li>
<li><b>description:</b> {returnValue?.description}</li>
</ul>
</>
Expand Down
8 changes: 2 additions & 6 deletions packages/playground/.storybook/args/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@ export interface IArgTypeEnhancer {
}

export type ReturnValue = {
type: {
text: string,
};
type: string;
description: string;
};

export type Parameter = {
name: string;
type: {
text: string
};
type: string;
description: string;
};

Expand Down
24 changes: 8 additions & 16 deletions packages/playground/build-scripts-storybook/parse-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ const loadManifest = () => {
try {
const customElementsMain = require("@ui5/webcomponents/custom-elements.json");
const customElementsFiori = require("@ui5/webcomponents-fiori/custom-elements.json");
const customElementsBase = require("@ui5/webcomponents-base/custom-elements.json");

return {
customElementsMain,
customElementsFiori,
customElementsBase,
};
} catch (error) {
console.log("Error while loading manifests. Did you run 'yarn build'?");
Expand All @@ -45,7 +43,6 @@ const loadManifest = () => {
return {
customElementsMain: {},
customElementsFiori: {},
customElementsBase: {},
};
}

Expand All @@ -59,6 +56,10 @@ const parseMembers = (members) => {
if (EXCLUDE_LIST.indexOf(member.name) > -1) {
return;
}
if (member.kind === "method") {
// change kind to property as Storybook does not show methods from the custom-elements.json
member.kind = "field";
}
parsed.push(member);
});
return parsed;
Expand All @@ -75,15 +76,6 @@ const parseModule = (module) => {
if (declaration.members) {
declaration.members = parseMembers(declaration.members);
}
// Storybook remove slots/css parts/properties/events with duplicate names so we add suffix to css parts in order to avoid duplicates.
// It can't happen to slots and properties since you can't have duplicate accessors.
if (declaration.cssParts) {
declaration.cssParts.forEach(part => {
if (!part.name.startsWith("_ui5") ) {
part.name = `_ui5${part.name}`;
}
});
}

return declaration;
});
Expand Down Expand Up @@ -125,7 +117,7 @@ const flattenAPIsHierarchicalStructure = module => {
}

const mergeClassMembers = (declaration, superclassDeclaration) => {
const props = ["members", "slots", "events", "cssParts"];
const props = ["members", "slots", "events"];

props.forEach(prop => {
if (declaration[prop]?.length) {
Expand All @@ -147,11 +139,11 @@ const mergeArraysWithoutDuplicates = (currentValues, newValue) => {
}


const { customElementsMain, customElementsFiori, customElementsBase } = loadManifest();
let customElements = mergeManifests(mergeManifests(customElementsMain, customElementsFiori), customElementsBase );
const { customElementsMain, customElementsFiori } = loadManifest();
const customElements = mergeManifests(customElementsMain, customElementsFiori );
const processedDeclarations = new Map();

customElements.modules.forEach(flattenAPIsHierarchicalStructure);
customElements.modules.forEach(flattenAPIsHierarchicalStructure)

fs.writeFileSync(
path.join(__dirname, "../.storybook/custom-elements.json"),
Expand Down
142 changes: 142 additions & 0 deletions packages/playground/build-scripts-storybook/samples-prepare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const fs = require('fs/promises');
const path = require('path');

const STORIES_ROOT_FOLDER_NAME = '../_stories';

// run the script to generate the argTypes for the stories available in the _stories folder
const main = async () => {

const baseAPI = JSON.parse((await fs.readFile(`../base/dist/api.json`)).toString());

// read all directories inside _stories folder and create a list of components
const packages = await fs.readdir(path.join(__dirname, STORIES_ROOT_FOLDER_NAME));
for (const package of packages) {
// packages [main, fiori]
const api = JSON.parse((await fs.readFile(`../${package}/dist/api.json`)).toString());

const packagePath = path.join(__dirname, STORIES_ROOT_FOLDER_NAME, package);
const packageStats = await fs.stat(packagePath);
if (packageStats.isDirectory()) {
const componentsInPackage = await fs.readdir(packagePath);
for (const component of componentsInPackage) {
// components [Button, Card, ...]
const componentPath = path.join(packagePath, component);
const componentStats = await fs.stat(componentPath);
if (componentStats.isDirectory()) {
generateStoryDoc(componentPath, component, api, package);
}
}
}
}

async function generateStoryDoc(componentPath, component, api, package) {
console.log(`Generating argTypes for story ${component}`);
const apiData = getAPIData(api, component, package);
const { storyArgsTypes, slotNames, info } = apiData;

await fs.writeFile(componentPath + '/argTypes.ts', `export default ${storyArgsTypes};
export const componentInfo = ${JSON.stringify(info, null, 4)};
export type StoryArgsSlots = {
${slotNames.map(slotName => `${slotName}: string;`).join('\n ')}
}`);
};

function getAPIData(api, module, package) {
const moduleAPI = api.symbols.find(s => s.module === module);
const data = getArgsTypes(api, moduleAPI);

return {
info: {
package: `@ui5/webcomponents${package !== 'main' ? `-${package}` : ''}`,
since: moduleAPI.since
},
slotNames: data.slotNames,
storyArgsTypes: JSON.stringify(data.args, null, "\t")
};
}

function getArgsTypes(api, moduleAPI) {
let args = {};
let slotNames = [];

moduleAPI?.properties?.forEach(prop => {
if (prop.visibility === 'public') {
const typeEnum = api.symbols.find(s => s.name === prop.type) || baseAPI.symbols.find(s => s.name === prop.type);
if (prop.readonly) {
args[prop.name] = {
control: {
type: false
},
};
} else if (Array.isArray(typeEnum?.properties)) {
args[prop.name] = {
control: "select",
options: typeEnum.properties.map(a => a.type),
};
}
}
});

moduleAPI?.slots?.forEach(prop => {
if (prop.visibility === 'public') {
args[prop.name] = {
control: {
type: "text"
}
};
slotNames.push(prop.name);
}
});

// methods parsing because Storybook does not include them in the args by default from the custom-elements.json
// only changing the category to Methods so they are not displayed in the Properties tab
moduleAPI?.methods?.forEach((prop) => {
if (prop.visibility === "public") {
args[prop.name] = {
description: prop.description,
table: {
category: "methods",
},
};

// methods can have custom descriptions with parameters and return value
if (prop.parameters || prop.returnValue) {
args[prop.name].UI5CustomData = {
parameters: prop.parameters,
returnValue: prop.returnValue,
}
}
}
});

// events also have custom descriptions with parameters of their detail object
moduleAPI?.events?.forEach((prop) => {
if (prop.visibility === "public" && prop.parameters) {
args[prop.name] = {
description: prop.description,
table: {
category: "events",
},
UI5CustomData: {
parameters: prop.parameters,
},
};
}
});

// recursively merging the args from the parent/parents
const moduleAPIBeingExtended = api.symbols.find(s => s.name === moduleAPI.extends) || baseAPI.symbols.find(s => s.module === moduleAPI.extends);
if (moduleAPIBeingExtended) {
const { args: nextArgs, slotNames: nextSlotNames } = getArgsTypes(api, moduleAPIBeingExtended);
args = { ...args, ...nextArgs };
slotNames = [...slotNames, ...nextSlotNames].filter((v, i, a) => a.indexOf(v) === i);
}

return {
args,
slotNames
};
}
};

main();
Loading

0 comments on commit 00e28cb

Please sign in to comment.