diff --git a/examples/react/.storybook/main.js b/examples/react/.storybook/main.js index a0863cef..30ae5e0e 100644 --- a/examples/react/.storybook/main.js +++ b/examples/react/.storybook/main.js @@ -12,4 +12,9 @@ module.exports = { // customize the Vite config here return config; }, + // Normally this wouldn't be necessary, but `react-docgen-typescript` is the default, + // and since typescript is installed in the monorepo, we can't rely on that check. + typescript: { + reactDocgen: 'react-docgen', + }, }; diff --git a/examples/react/.storybook/preview.js b/examples/react/.storybook/preview.js new file mode 100644 index 00000000..d3914580 --- /dev/null +++ b/examples/react/.storybook/preview.js @@ -0,0 +1,9 @@ +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; diff --git a/packages/builder-vite/package.json b/packages/builder-vite/package.json index 474d96cf..70171d49 100644 --- a/packages/builder-vite/package.json +++ b/packages/builder-vite/package.json @@ -18,10 +18,12 @@ "@storybook/csf-tools": "^6.3.3", "@storybook/source-loader": "^6.3.12", "@vitejs/plugin-react": "^1.0.8", + "ast-types": "^0.14.2", "es-module-lexer": "^0.9.3", "glob": "^7.2.0", "glob-promise": "^4.2.0", "magic-string": "^0.26.1", + "react-docgen": "^6.0.0-alpha.0", "slash": "^3.0.0", "vite-plugin-mdx": "^3.5.6" }, diff --git a/packages/builder-vite/plugins/docgen-handlers/actualNameHandler.ts b/packages/builder-vite/plugins/docgen-handlers/actualNameHandler.ts new file mode 100644 index 00000000..36b3a19c --- /dev/null +++ b/packages/builder-vite/plugins/docgen-handlers/actualNameHandler.ts @@ -0,0 +1,46 @@ +/** + * This is heavily based on the react-docgen `displayNameHandler` + * (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts) + * but instead defines an `actualName` property on the generated docs that is taken first from the component's actual name. + * This addresses an issue where the name that the generated docs are stored under is incorrectly named with the `displayName` + * and not the component's actual name. + * + * This is inspired by `actualNameHandler` from https://github.com/storybookjs/babel-plugin-react-docgen, but is modified + * directly from displayNameHandler, using the same approach as babel-plugin-react-docgen. + */ + +import { namedTypes as t } from 'ast-types'; +import type { NodePath } from 'ast-types/lib/node-path'; +import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/dist/utils'; +// import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/lib/utils'; +import type { Importer } from 'react-docgen/dist/parse'; +// import type { Importer } from 'react-docgen/lib/parse'; +import type Documentation from 'react-docgen/lib/Documentation'; + +export default function actualNameHandler(documentation: Documentation, path: NodePath, importer: Importer): void { + if (t.ClassDeclaration.check(path.node) || t.FunctionDeclaration.check(path.node)) { + documentation.set('actualName', getNameOrValue(path.get('id'))); + } else if ( + t.ArrowFunctionExpression.check(path.node) || + t.FunctionExpression.check(path.node) || + isReactForwardRefCall(path, importer) + ) { + let currentPath = path; + while (currentPath.parent) { + if (t.VariableDeclarator.check(currentPath.parent.node)) { + documentation.set('actualName', getNameOrValue(currentPath.parent.get('id'))); + return; + } else if (t.AssignmentExpression.check(currentPath.parent.node)) { + const leftPath = currentPath.parent.get('left'); + if (t.Identifier.check(leftPath.node) || t.Literal.check(leftPath.node)) { + documentation.set('actualName', getNameOrValue(leftPath)); + return; + } + } + currentPath = currentPath.parent; + } + // Could not find an actual name + documentation.set('actualName', ''); + } + return; +} diff --git a/packages/builder-vite/plugins/react-docgen.ts b/packages/builder-vite/plugins/react-docgen.ts new file mode 100644 index 00000000..e4d20cb1 --- /dev/null +++ b/packages/builder-vite/plugins/react-docgen.ts @@ -0,0 +1,53 @@ +import { + parse, + handlers as docgenHandlers, + resolver as docgenResolver, + importers as docgenImporters, +} from 'react-docgen'; +import type { DocumentationObject } from 'react-docgen/lib/Documentation'; +import MagicString from 'magic-string'; +import type { Plugin } from 'vite'; +import actualNameHandler from './docgen-handlers/actualNameHandler'; + +type DocObj = DocumentationObject & { actualName: string }; + +// TODO: None of these are able to be overridden, so `default` is aspirational here. +const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler); +const defaultResolver = docgenResolver.findAllExportedComponentDefinitions; +const defaultImporter = docgenImporters.makeFsImporter(); +const handlers = [...defaultHandlers, actualNameHandler]; + +export function reactDocgen(): Plugin { + return { + name: 'react-docgen', + enforce: 'pre', + async transform(src: string, id: string) { + // JSX syntax is only allowed in .tsx and .jsx, but components can technically be created without JSX + if (/\.(mjs|tsx?|jsx?)$/.test(id)) { + try { + // Since we're using `findAllExportedComponentDefinitions`, this will always be an array. + const docgenResults = parse(src, defaultResolver, handlers, { importer: defaultImporter, filename: id }) as + | DocObj[]; + const s = new MagicString(src); + + docgenResults.forEach((info) => { + const { actualName, ...docgenInfo } = info; + if (actualName) { + const docNode = JSON.stringify(docgenInfo); + s.append(`;${actualName}.__docgenInfo=${docNode}`); + } + }); + + return { + code: s.toString(), + map: s.generateMap(), + }; + } catch (e) { + // Usually this is just an error from react-docgen that it couldn't find a component + // Only uncomment for troubleshooting + // console.error(e); + } + } + }, + }; +} diff --git a/packages/builder-vite/types/react-docgen.d.ts b/packages/builder-vite/types/react-docgen.d.ts new file mode 100644 index 00000000..cc2a8d63 --- /dev/null +++ b/packages/builder-vite/types/react-docgen.d.ts @@ -0,0 +1,22 @@ +// TODO: delete this stub file once a new alpha of react-docgen is released (will include ts types). + +declare module 'react-docgen' { + declare const parse; + declare const handlers; + declare const resolver; + declare const importers; +} + +declare module 'react-docgen/lib/Documentation' { + export type DocumentationObject = Record; + export default Documentation; +} + +declare module 'react-docgen/dist/utils' { + declare const getNameOrValue; + declare const isReactForwardRefCall; +} + +declare module 'react-docgen/dist/parse' { + declare type Importer = any; +} diff --git a/packages/builder-vite/vite-config.ts b/packages/builder-vite/vite-config.ts index 5c19c1d9..74f4d705 100644 --- a/packages/builder-vite/vite-config.ts +++ b/packages/builder-vite/vite-config.ts @@ -116,6 +116,10 @@ export async function pluginConfig(options: ExtendedOptions, _type: PluginConfig if (reactDocgen === 'react-docgen-typescript' && typescriptPresent) { plugins.push(require('@joshwooding/vite-plugin-react-docgen-typescript').default(reactDocgenTypescriptOptions)); + } else if (reactDocgen) { + const { reactDocgen } = await import('./plugins/react-docgen'); + // Needs to run before the react plugin, so add to the front + plugins.unshift(reactDocgen()); } } diff --git a/yarn.lock b/yarn.lock index 923c5e0d..756e13a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3505,10 +3505,12 @@ __metadata: "@storybook/source-loader": ^6.3.12 "@types/express": ^4.17.13 "@vitejs/plugin-react": ^1.0.8 + ast-types: ^0.14.2 es-module-lexer: ^0.9.3 glob: ^7.2.0 glob-promise: ^4.2.0 magic-string: ^0.26.1 + react-docgen: ^6.0.0-alpha.0 slash: ^3.0.0 vite-plugin-mdx: ^3.5.6 vue-docgen-api: ^4.40.0 @@ -15742,6 +15744,27 @@ fsevents@^1.2.7: languageName: node linkType: hard +"react-docgen@npm:^6.0.0-alpha.0": + version: 6.0.0-alpha.0 + resolution: "react-docgen@npm:6.0.0-alpha.0" + dependencies: + "@babel/core": ^7.7.5 + "@babel/generator": ^7.12.11 + "@babel/runtime": ^7.7.6 + ast-types: ^0.14.2 + commander: ^2.19.0 + doctrine: ^3.0.0 + estree-to-babel: ^3.1.0 + neo-async: ^2.6.1 + node-dir: ^0.1.10 + resolve: ^1.17.0 + strip-indent: ^3.0.0 + bin: + react-docgen: bin/react-docgen.js + checksum: b1e7ad594a6191ca9e83d1d94e103db6d7e643ef69e9a5d7ca593f0f94e124bcbd48f89aaae8d4952b465781025c74fc7ee2dd7d433136e07a6f3e9529411416 + languageName: node + linkType: hard + "react-dom@npm:16.14.0, react-dom@npm:^16.4.14": version: 16.14.0 resolution: "react-dom@npm:16.14.0" @@ -16313,7 +16336,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"resolve@npm:^1.22.0": +"resolve@npm:^1.17.0, resolve@npm:^1.22.0": version: 1.22.0 resolution: "resolve@npm:1.22.0" dependencies: @@ -16336,7 +16359,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"resolve@patch:resolve@^1.22.0#~builtin": +"resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin": version: 1.22.0 resolution: "resolve@patch:resolve@npm%3A1.22.0#~builtin::version=1.22.0&hash=07638b" dependencies: