Created
February 8, 2021 18:01
-
-
Save kylpo/9ff293d06a880898b21072c35f2a2db5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This script is used to generate `argTypes` for Storybook. | |
* | |
* For helpful documentation, see: | |
* - https://github.com/storybookjs/storybook/blob/next/addons/controls/src/components/ControlsPanel.tsx#L14 | |
* - https://github.com/storybookjs/storybook/blob/next/lib/components/src/blocks/ArgsTable/ArgRow.tsx#L98 | |
* - https://github.com/storybookjs/storybook/blob/next/lib/components/src/blocks/ArgsTable/ArgValue.tsx#L129 | |
* | |
* @example <caption>Usage example for Avatar</caption> | |
* yarn docgen ./monorail/src/v2/core/Avatar/Avatar.tsx | |
* | |
*/ | |
const fs = require('fs') | |
const path = require('path') | |
const docgen = require('react-docgen-typescript') | |
const prettier = require('prettier') | |
const prettierConfig = require('../prettier.config') | |
const componentPath = process.argv[2] | |
const componentName = process.argv[3] || path.parse(componentPath).name | |
//#region Helpers | |
const DOUBLE_QUOTE_REGEX = RegExp(/"/g) | |
const PASCAL_CASE_REGEX = /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/ | |
const STARTS_WITH_CURLY = /^{/ | |
function buildDocgenCommand() { | |
let result = `yarn docgen ${componentPath}` | |
if (process.argv[3]) { | |
result += ` ${process.argv[3]}` | |
} | |
return result | |
} | |
/** | |
* @param {string} name | |
* @returns {Array<string>} | |
*/ | |
function getItemsFromName(name) { | |
// Assume "object" if starts with { or PascalCase | |
if (PASCAL_CASE_REGEX.test(name) || STARTS_WITH_CURLY.test(name)) { | |
return ['object'] | |
} | |
// For names that won't split well, just bail out | |
if (/[(){}[\]<>]/.test(name)) { | |
return [] | |
} | |
const splittedItems = name.split('|').map(value => value.trim()) | |
// Handle non-enum case | |
if (splittedItems.length === 1) { | |
return splittedItems | |
} | |
// Handle enum case | |
const items = splittedItems.map(item => | |
DOUBLE_QUOTE_REGEX.test(item) ? item.replace(DOUBLE_QUOTE_REGEX, '') : item, | |
) | |
return items | |
} | |
/** | |
* Build control object | |
* | |
* For all control options, see https://storybook.js.org/docs/react/essentials/controls#annotation | |
* | |
* @param {string} type | |
*/ | |
function buildControlForType(type) { | |
const items = getItemsFromName(type) | |
// case: invalid items | |
if (items.length === 0) { | |
return { type: null } | |
} | |
// case: non-enum type | |
else if (items.length === 1) { | |
const item = items[0] | |
switch (item) { | |
case 'string': | |
return { type: 'text' } | |
default: | |
return { type: item } | |
} | |
} | |
// case: enum with many items | |
else if (items.length > 4) { | |
return { type: 'select', options: items } | |
} | |
// case: enum with items | |
else { | |
return { type: 'radio', options: items } | |
} | |
} | |
/** | |
* @param {{required: boolean, type: {name: string}}} item | |
*/ | |
function transformPropDataForStorybook(item) { | |
// Handle enums by converting their values to unions | |
let type = | |
item.type.name === 'enum' | |
? item.type.value.map(({ value }) => value).join(' | ') | |
: item.type.name | |
return { | |
...item, | |
type: { ...item.type, required: item.required }, | |
table: { type: { summary: type } }, | |
control: buildControlForType(type), | |
} | |
} | |
//#endregion | |
//#region Generate argTypes | |
/* | |
* Create the parser with our typescript config | |
* | |
* Note: `shouldExtractValuesFromUnion` is disabled since it breaks optional types (everything becomes an enum) | |
* | |
* For more on these options, see: | |
* https://github.com/styleguidist/react-docgen-typescript/blob/master/src/parser.ts#L550 | |
*/ | |
const tsConfigParser = docgen.withCustomConfig('./tsconfig.json', { | |
shouldExtractLiteralValuesFromEnum: true, | |
shouldRemoveUndefinedFromOptional: true, | |
propFilter: (prop, component) => { | |
if (prop.parent) { | |
return ( | |
// Filter out standard HTML/DOM attributes | |
!prop.parent.fileName.includes('node_modules/@types/react/') && | |
// And specific parent names | |
![ | |
'StyledComponentProps', // (Mui) e.g. innerRef | |
'CommonProps', // (Mui) e.g. className, style | |
].includes(prop.parent.name) | |
) | |
} | |
return true | |
}, | |
}) | |
const parsedFile = tsConfigParser.parse(componentPath) | |
const parsedComponent = | |
parsedFile.find(component => component.displayName === componentName) || | |
parsedFile[0] | |
const parsedProps = parsedComponent.props | |
const unorderedArgTypes = {} | |
for (const [key, value] of Object.entries(parsedProps)) { | |
unorderedArgTypes[key] = transformPropDataForStorybook(value) | |
} | |
const argTypes = {} | |
Object.keys(unorderedArgTypes) | |
.sort((a, b) => { | |
// Sort required props to top | |
const aValue = unorderedArgTypes[a] | |
const bValue = unorderedArgTypes[b] | |
if (aValue.required && !bValue.required) { | |
return -1 | |
} | |
if (!aValue.required && bValue.required) { | |
return 1 | |
} | |
// Alphabetize the rest | |
return a.toLowerCase().localeCompare(b.toLowerCase()) | |
}) | |
.forEach(key => { | |
argTypes[key] = unorderedArgTypes[key] | |
}) | |
//#endregion | |
const content = { | |
'!!!!! DO NOT EDIT THIS FILE !!!!!!': { | |
'REGENERATE THIS WITH ONE OF THE FOLLOWING': [ | |
buildDocgenCommand(), | |
'yarn docgen:all', | |
], | |
}, | |
parameters: { | |
docs: { | |
description: { | |
component: parsedComponent.description, | |
}, | |
}, | |
}, | |
argTypes, | |
} | |
const formattedContent = prettier.format(JSON.stringify(content), { | |
...prettierConfig, | |
parser: 'json', | |
}) | |
const pathToComponentArgTypesFile = path.join( | |
componentPath, | |
'..', | |
'__stories__', | |
`${componentName}.meta.json`, | |
) | |
fs.writeFileSync(pathToComponentArgTypesFile, formattedContent, { | |
encoding: 'utf8', | |
flag: 'w', | |
}) | |
//#endregion | |
console.log(`Successfully overwrote ${pathToComponentArgTypesFile}`) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment