Skip to content
This repository has been archived by the owner on Jul 14, 2022. It is now read-only.

Provide stories with argTypes for primitives #3

Merged
merged 22 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .storybook/main.js
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"
}
9 changes: 9 additions & 0 deletions .storybook/preview.js
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$/,
},
},
}
44 changes: 44 additions & 0 deletions bin/generate-arg-types.ts
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}`)
})
24 changes: 20 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,36 @@
"repository": "git@github.com:webstudio-is/webstudio-sdk.git",
"homepage": "https://webstudio.is",
"scripts": {
"build": "yarn build:mdn-data && yarn build:prisma && yarn build:lib",
"build": "yarn build:mdn-data && yarn build:args && yarn build:prisma && yarn build:lib",
"build:mdn-data": "./bin/mdn-data.ts ./src/css",
"build:args": "./bin/generate-arg-types.ts './src/components/*.tsx !./src/**/*.stories.tsx'",
"build:prisma": "prisma format && prisma generate",
"typecheck": "tsc --noEmit",
"build:lib": "rm -fr lib && tsc",
"postinstall": "prisma generate",
"test": "jest",
"checks": "yarn typecheck && yarn test",
"prepublishOnly": "yarn typecheck && yarn build"
"prepublishOnly": "yarn typecheck && yarn build",
"storybook:run": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006",
"storybook:build": "NODE_OPTIONS=--openssl-legacy-provider build-storybook"
},
"devDependencies": {
"@babel/core": "^7.17.12",
"@remix-run/react": "^1.2.3",
"@remix-run/server-runtime": "^1.2.3",
"@storybook/addon-actions": "^6.5.6",
"@storybook/addon-essentials": "^6.5.6",
"@storybook/addon-interactions": "^6.5.6",
"@storybook/addon-links": "^6.5.6",
"@storybook/builder-webpack4": "^6.5.6",
"@storybook/manager-webpack4": "^6.5.6",
"@storybook/react": "^6.5.6",
"@storybook/testing-library": "^0.0.11",
"@types/css-tree": "^1.0.7",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"@types/react": "^17.0.40",
"babel-loader": "^8.2.5",
"camelcase": "^6.3.0",
"css-tree": "^2.1.0",
"esbuild": "^0.14.25",
Expand All @@ -32,12 +45,15 @@
"jest": "^27.5.1",
"mdn-data": "2.0.23",
"react": "^17.0.2",
"react-docgen-typescript": "^2.2.2",
"react-dom": "^17.0.2",
"typescript": "^4.6.2"
},
"peerDependencies": {
"@remix-run/react": "^1.2.3",
"@remix-run/server-runtime": "^1.2.3",
"react": "^17.0.2"
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"dependencies": {
"@prisma/client": "^3.10.0",
Expand Down
82 changes: 82 additions & 0 deletions src/arg-types/utils.ts
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 = {
Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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

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)) {
Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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 control property to the set and only export pure parsed prop types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, but then we would loose the interop with storybook

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. We could do enhance the data with controls in the designer or use already provided control property from CSF.

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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'}};
}
};
Loading