-
Notifications
You must be signed in to change notification settings - Fork 6
Provide stories with argTypes for primitives #3
Changes from all commits
a4bad83
4aa29f8
40283b1
bce4bda
6b7e54a
3e3c241
2e9bdc5
916bda8
5a9208d
fb1bbef
8c04a13
2b74c8a
4a5547c
fd6e91d
630e9c1
9872664
01987c8
47b7793
286f45c
c11ea4e
8d31cac
97fe83f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
module.exports = { | ||
"stories": [ | ||
"../src/**/*.stories.mdx", | ||
"../src/**/*.stories.@(js|jsx|ts|tsx)" | ||
], | ||
"addons": [ | ||
"@storybook/addon-links", | ||
"@storybook/addon-essentials", | ||
"@storybook/addon-interactions" | ||
], | ||
"framework": "@storybook/react" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const parameters = { | ||
actions: { argTypesRegex: "^on[A-Z].*" }, | ||
controls: { | ||
matchers: { | ||
color: /(background|color)$/i, | ||
date: /Date$/, | ||
}, | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
#!/usr/bin/env node --experimental-loader esbuild-node-loader | ||
|
||
import path from "path"; | ||
import docgen from "react-docgen-typescript"; | ||
import fg from "fast-glob" | ||
import fs from "fs-extra" | ||
import {propsToArgTypes} from "../src/arg-types/utils"; | ||
|
||
const options = { | ||
shouldExtractLiteralValuesFromEnum: true, | ||
shouldRemoveUndefinedFromOptional: true, | ||
} | ||
|
||
const componentsGlobString = process.argv.pop(); | ||
const tsConfigPath = path.resolve(process.cwd(), "./tsconfig.json"); | ||
|
||
if (typeof componentsGlobString === "undefined") { | ||
throw new Error("Please provide glob patterns (space separated) as arguments to match your components"); | ||
} | ||
|
||
// Search for components | ||
const globs = componentsGlobString.split(" "); | ||
const componentFiles = fg.sync(globs); | ||
|
||
console.log(`Resolved tscofig.json at ${tsConfigPath}\n`); | ||
console.log(`Glob patterns used: \n${globs.join('\n')}\n`) | ||
console.log(`Found files to process: \n${componentFiles.join('\n')}\n`) | ||
|
||
if (componentFiles.length === 0) { | ||
throw new Error("No component files found"); | ||
} | ||
|
||
// Create a parser with using your typescript config | ||
const tsConfigParser = docgen.withCustomConfig(tsConfigPath, options); | ||
|
||
// For each component file generate argTypes based on the propTypes | ||
componentFiles.forEach(filePath => { | ||
const jsonPath = filePath.replace('.tsx', '.props.json') | ||
const res = tsConfigParser.parse(filePath) | ||
const argTypes = propsToArgTypes(res[0].props) | ||
fs.ensureFileSync(jsonPath) | ||
fs.writeJsonSync(jsonPath, argTypes, {spaces: 2}) | ||
console.log(`Done generating argTypes for ${jsonPath}`) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import type {ArgTypes} from "@storybook/csf" | ||
import {PropItem} from "react-docgen-typescript"; | ||
|
||
export type FilterPredicate = (prop: PropItem) => boolean | ||
|
||
const validAttributes = (prop: PropItem) => { | ||
if (prop.parent) { | ||
// Pass *HTML (both ButtonHTMLAttributes and HTMLAttributes), Aria, and SVG attributes through | ||
const matcher = /.?(HTML|SVG|Aria)Attributes/; | ||
// TODO: Add a test for this | ||
return prop.parent.name.match(matcher); | ||
} | ||
// Always allow component's own props | ||
return true | ||
} | ||
|
||
export const propsToArgTypes = (props: Record<string, PropItem>, filter?: FilterPredicate): ArgTypes => { | ||
const filterFn = filter ?? validAttributes | ||
const entries = Object.entries(props); | ||
return entries | ||
.reduce((result, current) => { | ||
const [propName, prop] = current | ||
|
||
// Filter out props | ||
if (!filterFn(prop)) { | ||
return result | ||
} | ||
|
||
const control = mapControlForType(prop) | ||
result[propName] = {...prop, ...control} | ||
return result | ||
}, {} as ArgTypes); | ||
} | ||
|
||
const matchers = { | ||
color: new RegExp('(background|color)', 'i'), | ||
date: /Date$/ | ||
} | ||
|
||
export const mapControlForType = (propItem: PropItem): any => { | ||
const {type, name} = propItem; | ||
if (!type) { | ||
return undefined; | ||
} | ||
|
||
// args that end with background or color e.g. iconColor | ||
if (matchers.color && matchers.color.test(name)) { | ||
const controlType = propItem.type.name; | ||
|
||
if (controlType === 'string') { | ||
return { control: { type: 'color' }, defaultValue: propItem.defaultValue?.value }; | ||
} | ||
} | ||
|
||
// args that end with date e.g. purchaseDate | ||
if (matchers.date && matchers.date.test(name)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this might be too smart, because a field with purchaseDate could actually expect a non-date string or some kind of specific format and we would be probably enforcing an ISO date or something ... unless the type in typescript is an actual date, we probably shouldn't guess like this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree it's too "smart" and it's presentational logic that IMO should live in the designer, not in the data set. I would not even add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, but then we would loose the interop with storybook There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really. We could do enhance the data with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I imagine the future where a user can provide their components with stories and those could be potentially manually written, right? So in this case we say: we always generate argTypes ourselves, no matter what user has defined in the story? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if user doesn't have types ? :) |
||
return { control: { type: 'date' } }; | ||
} | ||
|
||
switch (type?.name) { | ||
case 'array': | ||
return {control: {type: 'object'}}; | ||
case 'boolean': | ||
case 'Booleanish': | ||
return {control: {type: 'boolean'}}; | ||
case 'string': | ||
return {control: {type: 'text'}}; | ||
case 'number': | ||
return {control: {type: 'number'}}; | ||
case 'enum': { | ||
const {value} = type; | ||
// @ts-expect-error Original type has `any` type | ||
const values = value.map(val => val.value) | ||
return {control: {type: values?.length <= 5 ? 'radio' : 'select'}, options: values}; | ||
} | ||
case 'function': | ||
case 'symbol': | ||
return null | ||
default: | ||
return {control: {type: 'text'}}; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs to be extended to support all controls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's how it's done in Storybook now. I would not do it here at all TBH.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, does this compromise our ability to render controls in both correctly? I guess if we do a better job at rendering than storybook its not a problem, unless we become incompatible