storybook argTypes没有正确生成TypeScript React组件的Props,该组件的接口扩展了另一个使用ComponentProps和Pick的类型,

ruarlubt  于 4个月前  发布在  TypeScript
关注(0)|答案(6)|浏览(60)

描述问题

在Create React App TypeScript项目中,我有一个FunctionComponent,它具有以下prop接口:

type PickedAntButtonProps = Pick<
  ComponentProps<typeof AntButton>,
  'size' | 'disabled' | 'icon' | 'loading' | 'onClick'
>;

export interface DefaultButtonProps extends PickedAntButtonProps {
  /**
* Set primary style
*/
  primary?: boolean;
  /**
* Fit button to parent width
*/
  stretch?: boolean;
}

Storybook无法正确识别这些props,只生成了 primarystretchonClick 的Controls & Doc Table条目,而控件似乎是Object定义( OnClick: {} )。
我不认为这是一个 react-docgen-typescript 的bug,因为我直接在组件上运行了parse,它似乎生成了正确的文档。

重现问题

前提条件:

  • CRA 4.0.3
  • antd添加到项目并设置(将antd作为依赖项并将css导入添加到index.ts)
  • 将Storybook添加到项目中

出现问题的组件:

import { ComponentProps, FunctionComponent } from 'react';
import { Button as AntButton } from 'antd';

type PickedAntButtonProps = Pick<
  ComponentProps<typeof AntButton>,
  'size' | 'disabled' | 'icon' | 'loading' | 'onClick'
>;

export interface DefaultButtonProps extends PickedAntButtonProps {
  /**
* Set primary style
*/
  primary?: boolean;
  /**
* Fit button to parent width
*/
  stretch?: boolean;
}

/**
* Default button
*/
const DefaultButton: FunctionComponent<DefaultButtonProps> = ({
  children,
  primary = true,
  stretch = false,
  ...props
}) => {
  return (
    <AntButton block={stretch} type={primary ? 'primary' : undefined} {...props} data-testid="default-button">
      {children}
    </AntButton>
  );
};

export default DefaultButton;

该组件返回并接受Ant Design按钮的一部分属性,并定义/重命名其中的一些属性。
运行storybook将显示以下Control和Doc Table中的props:
primary , stretch , onClick ,而 onClick 具有一个JSON控件,其中包含 onClick: {}
我尝试直接在文件上运行 react-docgen-typescript ,结果如下:

const docgen = require('react-docgen-typescript');

const options = {
  savePropValueAsString: true,
};

// Parse a file for docgen info
const docs = docgen.parse('./src/components/button/DefaultButton/DefaultButton.tsx', options);

console.log(JSON.stringify(docs, null, '\t'));

这看起来在我的眼中是正确的(-ish?),因此所有props都检测并正确解析,但Storybook似乎没有正确使用它们。
这可能是我这边的一个配置问题,或者是Storybook没有正确检测某些东西。我在下面进一步添加了我的 main.js 。也可能问题完全不同,与这个特殊的类型无关。

系统

System:
    OS: macOS 10.15.7
    CPU: (8) x64 Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
  Binaries:
    Node: 14.16.1 - ~/.nvm/versions/node/v14.16.1/bin/node
    Yarn: 1.22.10 - ~/.nvm/versions/node/v14.16.1/bin/yarn
    npm: 6.14.12 - ~/.nvm/versions/node/v14.16.1/bin/npm
  Browsers:
    Chrome: 90.0.4430.93
    Safari: 14.0.3
  npmPackages:
    @storybook/addon-actions: ^6.2.9 => 6.2.9
    @storybook/addon-essentials: ^6.2.9 => 6.2.9
    @storybook/addon-links: ^6.2.9 => 6.2.9
    @storybook/node-logger: ^6.2.9 => 6.2.9
    @storybook/preset-ant-design: 0.0.2 => 0.0.2
    @storybook/preset-create-react-app: ^3.1.7 => 3.1.7
    @storybook/react: ^6.2.9 => 6.2.9

附加上下文

Storybook启动命令: start-storybook -p 6006 -s public
.storybook/main.js :
我有各种配置来添加导入别名,基于口味的不同ant主题(这是一个白标签应用程序),以及ant design。我已经包含了几乎所有的东西,所以如果只是一个配置问题,那么找到问题应该会更容易。

const path = require('path');

/*
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["./src/components/*"]
}
}
}
*/
/*
and in tsconfig.json:
{
"extends": "./tsconfig.paths.json",
"compilerOptions": {
...
*/
const aliasPaths = require(path.join(__dirname, '/../tsconfig.paths.json'));

/*
const path = require('path');

const antThemes = {
a: require(path.join(__dirname, '/a/theme.ant.json')),
b: require(path.join(__dirname, '/b/theme.ant.json')),
c: require(path.join(__dirname, '/c/theme.ant.json')),
};

module.exports = {
antThemes,
};
*/
/*
{
"@primary-color": "#ff3131",
"@text-color": "rgba(0, 0, 0, 0.85)",
"@btn-primary-color": "#ffffff"
}
*/
const antThemes = require(path.join(__dirname, '/../src/settings/themes.js')).antThemes;

// SNIP - Removed some logic to determine which flavor to build as this app uses different themes for whitelabling

/*
* Auto Generate Path Aliases
* Using the tsconfig.paths.json config file, generate WebPack path aliases
*/
const matchAliasName = /(.*)\/\*/;
const matchAliasPath = /\.\/(.*)\/\*/;
const webpackPathAliases = Object.entries(aliasPaths.compilerOptions.paths)
  .map((entry) => {
    const aliasNameMatch = entry[0].match(matchAliasName);
    const aliasPathMatch = entry[1][0].match(matchAliasPath);
    return [aliasNameMatch[1], aliasPathMatch[1]];
  })
  .reduce((acc, entry) => {
    acc[entry[0]] = path.join(__dirname, `/../${entry[1]}`);
    return acc;
  }, {});

/*
* Main Storybook Config
*/
module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    {
      name: '@storybook/preset-create-react-app',
      options: {
        craOverrides: {
          fileLoaderExcludes: ['less'],
        },
      },
    },
    {
      name: '@storybook/preset-ant-design',
      options: {
        lessOptions: {
          modifyVars: antThemes[targetFlavor],
        },
      },
    },
  ],
  webpackFinal: (config) => {
    /*
* Push REACT_APP_ environment variables into storybook webpack config.
* Otherwise only STORYBOOK_ variables would be available.
*/
    const plugin = config.plugins.find((plugin) => plugin.definitions?.['process.env']);
    const reactAppEnv = Object.keys(process.env)
      .filter((name) => /^REACT_APP_/.test(name))
      .reduce((acc, name) => {
        acc[name] = `"${process.env[name]}"`;
        return acc;
      }, {});
    plugin.definitions['process.env'] = {
      ...plugin.definitions['process.env'],
      ...reactAppEnv,
    };

    return {
      ...config,
      resolve: {
        ...config.resolve,
        alias: {
          ...config.resolve.alias,
          ...webpackPathAliases,
        },
      },
    };
  },
};

preview.js 只进行了轻微修改,有两个装饰器用于Suspense和i18n Providers,以及一个globalTypes导出用于更改Locale的按钮:

import { Suspense, useEffect } from 'react';
import { I18nextProvider } from 'react-i18next';
import '../src/index.css'; // Load global styles
import AppConfig from '../src/settings/config'; // Load app flavor theme
import i18n from '../src/services/i18n/i18next';

/*
* Define Storybook globals
*/
export const globalTypes = {
  locale: {
    name: 'Locale',
    description: 'i18n locale',
    defaultValue: 'de',
    toolbar: {
      icon: 'globe',
      items: [
        { value: 'en', right: '🇺🇸', title: 'English' },
        { value: 'de', right: '🇩🇪', title: 'Deutsch' },
      ],
    },
  },
};

/*
* react-i18next wrapper to change locale based on storybook global
*/
const I18nProviderWrapper = ({ children, i18n, locale }) => {
  useEffect(() => {
    i18n.changeLanguage(locale);
  }, [i18n, locale]);
  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
};

export const parameters = {
  appFlavor: AppConfig.flavor,
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

/*
* Story wrappers
*/
export const decorators = [
  (Story, { globals }) => (
    <I18nProviderWrapper locale={globals.locale} i18n={i18n}>
      <Story />
    </I18nProviderWrapper>
  ),
  (Story) => (
    <Suspense fallback="loading">
      <Story />
    </Suspense>
  ),
];
1sbrub3j

1sbrub3j1#

看起来我可能说得太早了,这(至少部分上)是一个react-docgen-typescript的问题,因为它似乎与:styleguidist/react-docgen-typescript#68styleguidist/react-docgen-typescript#56有关。
在上面的示例中,一个可能的修复/解决方法是将接口更改为:

import { ButtonProps as AntButtonProps } from 'antd';

export interface DefaultButtonProps {
  size?: AntButtonProps['size'];
  disabled?: AntButtonProps['disabled'];
  icon?: AntButtonProps['icon'];
  loading?: AntButtonProps['loading'];
  onClick?: AntButtonProps['onClick'];
  className?: AntButtonProps['className'];
  /**
* Set primary style
*/
  primary?: boolean;
  /**
* Fit button to parent width
*/
  stretch?: boolean;
}

这将生成正确的控件,但要冗长得多。
走更直接的Pick<AntButtonProps, 'size' | 'disabled' | 'icon' | 'loading' | 'onClick' | 'className'>的方法也不起作用。

agyaoht7

agyaoht72#

遇到相同的问题。如果我们不必重新创建仅链接到现有类型的类型,那就太好了。

idfiyjo8

idfiyjo83#

在这里也遇到了同样的问题。

tvokkenx

tvokkenx4#

经过一些挖掘,这个对我有用:

// main.js
import { mergeConfig, UserConfig } from 'vite';
import { config as viteConfig } from './vite.config';
import * as reactDocgen from 'react-docgen-typescript';
export default {
  framework: '@storybook/react',
  stories: ['../stories/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    'storybook-addon-designs/preset',
    '@storybook/addon-docs',
  ],
  core: {
    builder: '@storybook/builder-vite',
    disableTelemetry: true,
  },
  features: {
    storyStoreV7: true,
  },
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      // By default react-doc-gen-typescript filters node_modules type, this includes antd types
      tsconfigPath: '../tsconfig.json',
      propFilter: (prop: any) => {
        const res = /antd/.test(prop.parent?.fileName) || !/node_modules/.test(prop.parent?.fileName);
        return prop.parent ? res : true;
      },
      // The following 2 options turns string types into string literals and allows
      shouldExtractLiteralValuesFromEnum: true,
      savePropValueAsString: true,
    },
  },
  async viteFinal(config: UserConfig, { configType }: { configType: string }) {
    console.log('configType', config);
    return mergeConfig(config, viteConfig({ command: 'serve', mode: configType.toLowerCase() }));
  },
};
zy1mlcev

zy1mlcev5#

遇到了类似的问题。
最后通过以下 filterProp 函数解决了:

propFilter: (prop) => {
  if (prop.name === 'children') {
    return true;
  }

  if (prop.parent) {
    return !/node_modules/.test(prop.parent?.fileName);
  }

  // Exclude native HTML props
  if (!prop.declarations) {
    return false;
  }

  return true;
},
kkbh8khc

kkbh8khc6#

经过一些挖掘,这个方法对我有效:

// main.js
import { mergeConfig, UserConfig } from 'vite';
import { config as viteConfig } from './vite.config';
import * as reactDocgen from 'react-docgen-typescript';
export default {
  framework: '@storybook/react',
  stories: ['../stories/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    'storybook-addon-designs/preset',
    '@storybook/addon-docs',
  ],
  core: {
    builder: '@storybook/builder-vite',
    disableTelemetry: true,
  },
  features: {
    storyStoreV7: true,
  },
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      // By default react-doc-gen-typescript filters node_modules type, this includes antd types
      tsconfigPath: '../tsconfig.json',
      propFilter: (prop: any) => {
        const res = /antd/.test(prop.parent?.fileName) || !/node_modules/.test(prop.parent?.fileName);
        return prop.parent ? res : true;
      },
      // The following 2 options turns string types into string literals and allows
      shouldExtractLiteralValuesFromEnum: true,
      savePropValueAsString: true,
    },
  },
  async viteFinal(config: UserConfig, { configType }: { configType: string }) {
    console.log('configType', config);
    return mergeConfig(config, viteConfig({ command: 'serve', mode: configType.toLowerCase() }));
  },
};

我尝试了这个方法,但它并不适用于所有组件,例如按钮组件,但对于自动完成功能来说效果很好。

相关问题